]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
baculum: Implement autochanger management
authorMarcin Haba <marcin.haba@bacula.pl>
Sun, 4 Apr 2021 21:01:49 +0000 (23:01 +0200)
committerMarcin Haba <marcin.haba@bacula.pl>
Sun, 4 Apr 2021 21:01:49 +0000 (23:01 +0200)
58 files changed:
gui/baculum/examples/selinux/baculum-api.te
gui/baculum/protected/API/Class/ChangerCommand.php [new file with mode: 0644]
gui/baculum/protected/API/Class/DeviceConfig.php [new file with mode: 0644]
gui/baculum/protected/API/Class/VolumeManager.php
gui/baculum/protected/API/Lang/en/messages.mo
gui/baculum/protected/API/Lang/en/messages.po
gui/baculum/protected/API/Lang/pl/messages.mo
gui/baculum/protected/API/Lang/pl/messages.po
gui/baculum/protected/API/Lang/pt/messages.mo
gui/baculum/protected/API/Lang/pt/messages.po
gui/baculum/protected/API/Lang/ru/messages.mo
gui/baculum/protected/API/Lang/ru/messages.po
gui/baculum/protected/API/Pages/API/ChangerDriveLoad.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/ChangerDriveLoaded.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/ChangerDriveUnload.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/ChangerList.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/ChangerListAll.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/ChangerSlots.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/ChangerSlotsTransfer.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/StorageMount.php
gui/baculum/protected/API/Pages/API/StorageMountV1.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/StorageRelease.php
gui/baculum/protected/API/Pages/API/StorageReleaseV1.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/StorageUmount.php
gui/baculum/protected/API/Pages/API/StorageUmountV1.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/API/VolumeLabel.php
gui/baculum/protected/API/Pages/API/config.xml
gui/baculum/protected/API/Pages/API/endpoints.xml
gui/baculum/protected/API/Pages/Panel/APIDevices.page [new file with mode: 0644]
gui/baculum/protected/API/Pages/Panel/APIDevices.php [new file with mode: 0644]
gui/baculum/protected/API/Pages/Panel/APIInstallWizard.page
gui/baculum/protected/API/Pages/Panel/APISettings.page
gui/baculum/protected/API/Pages/Panel/config.xml
gui/baculum/protected/API/Pages/Panel/endpoints.xml
gui/baculum/protected/API/Portlets/APISideBar.tpl
gui/baculum/protected/API/Portlets/SudoConfig.php [new file with mode: 0644]
gui/baculum/protected/API/Portlets/SudoConfig.tpl [new file with mode: 0644]
gui/baculum/protected/Common/Class/BClientScript.php
gui/baculum/protected/Common/Class/Errors.php
gui/baculum/protected/Common/Class/OAuth2.php
gui/baculum/protected/Common/JavaScript/misc.js
gui/baculum/protected/Web/JavaScript/misc.js
gui/baculum/protected/Web/Lang/en/messages.mo
gui/baculum/protected/Web/Lang/en/messages.po
gui/baculum/protected/Web/Lang/ja/messages.mo
gui/baculum/protected/Web/Lang/ja/messages.po
gui/baculum/protected/Web/Lang/pl/messages.mo
gui/baculum/protected/Web/Lang/pl/messages.po
gui/baculum/protected/Web/Lang/pt/messages.mo
gui/baculum/protected/Web/Lang/pt/messages.po
gui/baculum/protected/Web/Lang/ru/messages.mo
gui/baculum/protected/Web/Lang/ru/messages.po
gui/baculum/protected/Web/Pages/StorageView.page
gui/baculum/protected/Web/Pages/StorageView.php
gui/baculum/protected/Web/Portlets/LabelVolume.php
gui/baculum/protected/Web/Portlets/LabelVolume.tpl
gui/baculum/protected/Web/Portlets/UpdateSlots.php
gui/baculum/protected/Web/Portlets/UpdateSlots.tpl

index bf1c53e5f49068492e1653872c8bf9ff0855de20..e7ceecd0a5c018edd6de43281cd9e309568d45c4 100644 (file)
@@ -1,4 +1,4 @@
-module baculum-api 1.0.2;
+module baculum-api 1.0.3;
 
 require {
        type init_t;
@@ -12,21 +12,30 @@ require {
        type httpd_cache_t;
        type bacula_etc_t;
        type bacula_exec_t;
+       type bacula_spool_t;
        type httpd_sys_rw_content_t;
+       type scsi_generic_device_t;
        type shadow_t;
        type systemd_systemctl_exec_t;
+       type systemd_logind_sessions_t;
+       type systemd_logind_t;
        type systemd_unit_file_t;
        type admin_home_t;
        type usr_t;
        type postfix_etc_t;
+       type initrc_var_run_t;
+       type tape_device_t;
        class tcp_socket { name_bind name_connect };
-       class dir { search read write create };
-       class file { append read write create getattr open execute execute_no_trans };
+       class dir { search read write create add_name remove_name };
+       class file { append read write create getattr setattr open execute execute_no_trans ioctl unlink lock rename };
+       class chr_file { open read write ioctl };
+       class fifo_file { write };
        class netlink_audit_socket { write nlmsg_relay create read };
        class capability { audit_write sys_resource net_admin };
        class service { start stop };
        class unix_stream_socket { connectto };
        class process { setrlimit };
+       class dbus { send_msg };
 }
 
 #============= httpd_t ==============
@@ -38,6 +47,9 @@ allow httpd_t hplip_port_t:tcp_socket name_connect;
 allow httpd_t bacula_etc_t:dir { read write search };
 allow httpd_t bacula_etc_t:file { getattr read write open };
 allow httpd_t bacula_exec_t:file { getattr read execute execute_no_trans open };
+allow httpd_t bacula_spool_t:dir { write add_name remove_name };
+allow httpd_t bacula_spool_t:file { getattr create open read write ioctl unlink };
+allow httpd_t scsi_generic_device_t:chr_file { open read write ioctl };
 allow httpd_t sudo_exec_t:file { read execute open };
 allow httpd_t httpd_cache_t:dir { read create };
 allow httpd_t httpd_cache_t:file { read write create };
@@ -48,8 +60,16 @@ allow httpd_t httpd_sys_rw_content_t:dir { read write };
 allow httpd_t httpd_sys_rw_content_t:file { create append };
 allow httpd_t shadow_t:file { open read getattr };
 allow httpd_t systemd_systemctl_exec_t:file { getattr open read execute execute_no_trans };
+allow httpd_t systemd_logind_sessions_t:fifo_file write;
+allow httpd_t systemd_logind_t:dbus send_msg;
 allow httpd_t systemd_unit_file_t:service { start stop };
 allow httpd_t init_t:unix_stream_socket connectto;
-allow httpd_t admin_home_t:file { getattr open read append write };
+allow httpd_t admin_home_t:dir { write add_name remove_name };
+allow httpd_t admin_home_t:file { getattr setattr create open read append write rename unlink };
 allow httpd_t usr_t:file write;
 allow httpd_t postfix_etc_t:file read;
+allow httpd_t initrc_var_run_t:file { open read lock };
+allow httpd_t tape_device_t:chr_file read;
+
+#============= systemd_logind_t ==============
+allow systemd_logind_t httpd_t:dbus send_msg;
diff --git a/gui/baculum/protected/API/Class/ChangerCommand.php b/gui/baculum/protected/API/Class/ChangerCommand.php
new file mode 100644 (file)
index 0000000..490f3e1
--- /dev/null
@@ -0,0 +1,366 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.Common.Class.Errors');
+Prado::using('Application.API.Class.DeviceConfig');
+Prado::using('Application.API.Class.APIModule');
+
+/**
+ * Execute changer command module.
+ * Changer command should provide interface compatible with
+ * mtx-changer script provided with Bacula.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category Autochanger
+ * @package Baculum API
+ */
+class ChangerCommand extends APIModule {
+
+       const SUDO = 'sudo';
+
+       /**
+        * Types to determine how changer command is executed (foreground, background...)
+        */
+       const PTYPE_FG_CMD = 0;
+       const PTYPE_BG_CMD = 1;
+
+       /**
+        * Output file prefix used to temporary store output from commands.
+        */
+       const OUTPUT_FILE_PREFIX = 'output_';
+
+       /**
+        * Pattern to changer command.
+        */
+       const CHANGER_COMMAND_FG_PATTERN = '%s %s 2>&1';
+       const CHANGER_COMMAND_BG_PATTERN = '{ %s %s 1>%s 2>&1; echo "quit" >> %s ; } &';
+
+       /**
+        * Supported parameters with short codes.
+        * NOTE: order has meaning here.
+        */
+       private $params = [
+               '%c' => 'changer-device',
+               '%o' => 'command',
+               '%S' => 'slot',
+               '%a' => 'archive-device',
+               '%d' => 'drive-index'
+       ];
+
+       /**
+        * Supported changer commands.
+        */
+       private $commands = [
+               'load',
+               'unload',
+               'loaded',
+               'list',
+               'slots',
+               'listall',
+               'transfer'
+       ];
+
+       /**
+        * Stores device config.
+        */
+       private $config;
+
+       public function init($param) {
+               $this->config = $this->getModule('device_config')->getConfig();
+       }
+
+       /**
+        * Validate changer script command.
+        * @param string $command script command
+        * @return boolean true on validation success, otherwise false
+        */
+       private function validateCommand($command) {
+               return in_array($command, $this->commands);
+       }
+
+       /**
+        * Get sudo command.
+        *
+        * @param boolean $use_sudo sudo option state
+        * @return string sudo command
+        */
+       private function getSudo($use_sudo) {
+               $sudo = '';
+               if ($use_sudo === true) {
+                       $sudo = self::SUDO;
+               }
+               return $sudo;
+       }
+
+       /**
+        * Execute changer command.
+        *
+        * @param string $changer autochanger device name
+        * @param string $command changer command (load, unload ...etc.)
+        * @param string $device archive device name (autochanger drive name)
+        * @param string $slot slot in slots magazine to use
+        * @param string $slotdest destination slot in slots magazine (used with transfer command)
+        * @param string $ptype command pattern type
+        * @return StdClass executed command output and error code
+        */
+       public function execChangerCommand($changer, $command, $device = null, $slot = null, $slotdest = null, $ptype = null) {
+               if (!$this->validateCommand($command)) {
+                       $output = DeviceError::MSG_ERROR_DEVICE_INVALID_COMMAND;
+                       $error = DeviceError::ERROR_DEVICE_INVALID_COMMAND;
+                       $result = $this->prepareResult($output, $error);
+                       return $result;
+               }
+               if (count($this->config) == 0)  {
+                       $output = DeviceError::MSG_ERROR_DEVICE_DEVICE_CONFIG_DOES_NOT_EXIST;
+                       $error = DeviceError::ERROR_DEVICE_DEVICE_CONFIG_DOES_NOT_EXIST;
+                       $result = $this->prepareResult($output, $error);
+                       return $result;
+               }
+               if (!key_exists($changer, $this->config) || $this->config[$changer]['type'] !== DeviceConfig::DEV_TYPE_AUTOCHANGER)  {
+                       $output = DeviceError::MSG_ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST;
+                       $error = DeviceError::ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST;
+                       $result = $this->prepareResult($output, $error);
+                       return $result;
+               }
+               if (is_string($device)) {
+                       $drives = explode(',', $this->config[$changer]['drives']);
+                       if (!in_array($device, $drives))  {
+                               $output = DeviceError::MSG_ERROR_DEVICE_DRIVE_DOES_NOT_BELONG_TO_AUTOCHANGER;
+                               $error = DeviceError::ERROR_DEVICE_DRIVE_DOES_NOT_BELONG_TO_AUTOCHANGER;
+                               $result = $this->prepareResult($output, $error);
+                               return $result;
+                       }
+               }
+
+               if (is_string($device) && (!key_exists($device, $this->config) || $this->config[$device]['type'] !== DeviceConfig::DEV_TYPE_DEVICE))  {
+                       $output = DeviceError::MSG_ERROR_DEVICE_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       $error = DeviceError::ERROR_DEVICE_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       $result = $this->prepareResult($output, $error);
+                       return $result;
+               }
+               $changer_command = $this->config[$changer]['command'];
+               $changer_device = $this->config[$changer]['device'];
+               $archive_device = is_string($device) ? $this->config[$device]['device'] : '';
+               $drive_index = is_string($device) ? $this->config[$device]['index'] : '';
+               $use_sudo = ($this->config[$changer]['use_sudo'] == 1);
+
+               if ($command === 'transfer') {
+                       // in transfer command in place archive device is given destination slot
+                       $archive_device = $slotdest;
+               }
+
+               $command = $this->prepareChangerCommand(
+                       $changer_command,
+                       $changer_device,
+                       $command,
+                       $slot,
+                       $archive_device,
+                       $drive_index
+               );
+               $pattern = $this->getCmdPattern($ptype);
+               $cmd = $this->getCommand($pattern, $use_sudo, $command);
+               $result = $this->execCommand($cmd, $ptype);
+               if ($result->error !== 0) {
+                       $emsg = PHP_EOL . ' Output:' . implode(PHP_EOL, $result->output);
+                       $output = DeviceError::MSG_ERROR_WRONG_EXITCODE . $emsg;
+                       $exitcode = DeviceError::ERROR_WRONG_EXITCODE;
+                       $result = $this->prepareResult($output, $exitcode);
+               }
+               return $result;
+       }
+
+       /**
+        * Prepare changer command to execute.
+        *
+        * @param string $changer_command full changer command
+        * @param string $changer_device changer device name
+        * @param string $command changer command (load, unload ...etc.)
+        * @param string $slot slot in slots magazine to use
+        * @param string $archive_device archive device name (autochanger drive name)
+        * @param string $drive_index archive device index (autochanger drive index)
+        * @return StdClass executed command output and error code
+        */
+       private function prepareChangerCommand($changer_command, $changer_device, $command, $slot, $archive_device, $drive_index) {
+               $from = array_keys($this->params);
+               $to = [
+                       '"' . $changer_device .'"',
+                       '"' . $command .'"',
+                       '"' . $slot .'"',
+                       '"' . $archive_device .'"',
+                       '"' . $drive_index .'"'
+               ];
+               return str_replace($from, $to, $changer_command);
+       }
+
+       /**
+        * Get changer command to execute.
+        *
+        * @param string $pattern changer command pattern (@see PTYPE_ constants)
+        * @param boolean $use_sudo information about using sudo
+        * @param string $bin changer command
+        * @return array changer command (and output id if selected pattern to
+        * move command to background)
+        */
+       private function getCommand($pattern, $use_sudo, $bin) {
+               $command = array('cmd' => null, 'out_id' => null);
+               $misc = $this->getModule('misc');
+               $sudo = $this->getSudo($use_sudo);
+
+               if ($pattern === self::CHANGER_COMMAND_BG_PATTERN) {
+                       $file = $this->prepareOutputFile();
+                       $cmd = sprintf(
+                               $pattern,
+                               $sudo,
+                               $bin,
+                               $file,
+                               $file
+                       );
+                       $command['cmd'] = $misc->escapeCharsToConsole($cmd);
+                       $command['out_id'] = preg_replace('/^[\s\S]+\/' . self::OUTPUT_FILE_PREFIX . '/', '', $file);
+               } else {
+                       $cmd = sprintf($pattern, $sudo, $bin);
+                       $command['cmd'] = $misc->escapeCharsToConsole($cmd);
+                       $command['out_id'] = '';
+               }
+               return $command;
+       }
+
+       /**
+        * Create and get output file.
+        * Used with background type command patterns (ex. PTYPE_BG_CMD)
+        *
+        * @return string|boolean new temporary filename (with path), or false on failure.
+        */
+       private function prepareOutputFile() {
+               $dir = Prado::getPathOfNamespace('Application.API.Config');
+               $fname = tempnam($dir, self::OUTPUT_FILE_PREFIX);
+               return $fname;
+       }
+
+       /**
+        * Read output file and return the output.
+        * Used with background type command patterns (ex. PTYPE_BG_CMD)
+        *
+        * @param string $out_id command output identifier
+        * @return array command output with one line per one array element
+        */
+       public static function readOutputFile($out_id) {
+               $output = [];
+               $dir = Prado::getPathOfNamespace('Application.API.Config');
+               if (preg_match('/^[a-z0-9]+$/i', $out_id) === 1) {
+                       $file = $dir . '/' . self::OUTPUT_FILE_PREFIX . $out_id;
+                       if (file_exists($file)) {
+                               $output = file($file);
+                       }
+                       $output_count = count($output);
+                       $last = $output_count > 0 ? trim($output[$output_count-1]) : '';
+                       if ($last === 'quit') {
+                               // output is complete, so remove the file
+                               unlink($file);
+                       }
+               }
+               return $output;
+       }
+
+       /**
+        * Execute changer command.
+        *
+        * @param string $bin command
+        * @param string $ptype command pattern type
+        * @return array result with output and error code
+        */
+       public function execCommand($cmd, $ptype = null) {
+               exec($cmd['cmd'], $output, $exitcode);
+               $this->getModule('logging')->log(
+                       $cmd['cmd'],
+                       $output,
+                       Logging::CATEGORY_EXECUTE,
+                       __FILE__,
+                       __LINE__
+               );
+               if ($ptype === self::PTYPE_BG_CMD) {
+                       $output = [
+                               'out_id' => $cmd['out_id']
+                       ];
+               }
+               return $this->prepareResult($output, $exitcode);
+       }
+
+       /**
+        * Prepare changer command result.
+        *
+        * @param array $output output from command execution
+        * @param integer $error command error code
+        * @return array result with output and error code
+        */
+       public function prepareResult($output, $error) {
+               $result = new StdClass;
+               $result->output = $output;
+               $result->error  = $error;
+               return $result;
+       }
+
+       /**
+        * Get command pattern by ptype.
+        *
+        * @param string $ptype pattern type (@see PTYPE_ constants)
+        * @return string command pattern
+        */
+       private function getCmdPattern($ptype) {
+               $pattern = null;
+               switch ($ptype) {
+                       case self::PTYPE_FG_CMD: $pattern = self::CHANGER_COMMAND_FG_PATTERN; break;
+                       case self::PTYPE_BG_CMD: $pattern = self::CHANGER_COMMAND_BG_PATTERN; break;
+                       default: $pattern = self::CHANGER_COMMAND_FG_PATTERN;
+               }
+               return $pattern;
+       }
+
+       /**
+        * Check changer command parameters.
+        * Used to test parameters.
+        *
+        * @param boolean $use_sudo information about using sudo
+        * @param string $changer_command full changer command
+        * @param string $changer_device changer device name
+        * @param string $command changer command (load, unload ...etc.)
+        * @param string $slot slot in slots magazine to use
+        * @param string $archive_device archive device name (autochanger drive name)
+        * @param string $drive_index archive device index (autochanger drive index)
+        * @return StdClass executed command output and error code
+        */
+       public function testChangerCommand($use_sudo, $changer_command, $changer_device, $command, $slot, $archive_device, $drive_index) {
+               $command = $this->prepareChangerCommand(
+                       $changer_command,
+                       $changer_device,
+                       $command,
+                       $slot,
+                       $archive_device,
+                       $drive_index
+               );
+               $pattern = $this->getCmdPattern(self::PTYPE_FG_CMD);
+               $cmd = $this->getCommand($pattern, $use_sudo, $command);
+               $result = $this->execCommand($cmd, self::PTYPE_FG_CMD);
+               return $result;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Class/DeviceConfig.php b/gui/baculum/protected/API/Class/DeviceConfig.php
new file mode 100644 (file)
index 0000000..cdc92be
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.Common.Class.ConfigFileModule');
+
+/**
+ * Manage devices configuration.
+ * Module is responsible for device config data.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category Device
+ * @package Baculum API
+ */
+class DeviceConfig extends ConfigFileModule {
+
+       /**
+        * Supported device types
+        */
+       const DEV_TYPE_DEVICE = 'device';
+       const DEV_TYPE_AUTOCHANGER = 'autochanger';
+
+       /**
+        * Device file path patter.
+        */
+       const DEVICE_PATH_PATTERN = '[a-zA-Z0-9:.\-_ ]+';
+
+       /**
+        * Device config file path
+        */
+       const CONFIG_FILE_PATH = 'Application.API.Config.devices';
+
+       /**
+        * Device config file format
+        */
+       const CONFIG_FILE_FORMAT = 'ini';
+
+       /**
+        * Stores device config.
+        */
+       private $config = null;
+
+       /**
+        * These options are obligatory for device config.
+        */
+       private $required_options = ['type', 'device'];
+
+       /**
+        * Get (read) device config.
+        *
+        * @param string $section config section name
+        * @return array config
+        */
+       public function getConfig($section = null) {
+               $config = [];
+               if (is_null($this->config)) {
+                       $this->config = $this->readConfig(self::CONFIG_FILE_PATH, self::CONFIG_FILE_FORMAT);
+               }
+               $is_valid = true;
+               if (!is_null($section)) {
+                       $config = key_exists($section, $this->config) ? $this->config[$section] : [];
+                       $is_valid = $this->validateConfig($section, $config);
+               } else {
+                       foreach ($this->config as $section => $value) {
+                               if ($this->validateConfig($section, $value) === false) {
+                                       $is_valid = false;
+                                       break;
+                               }
+                               $config[$section] = $value;
+                       }
+               }
+               if ($is_valid === false) {
+                       // no validity, no config
+                       $config = [];
+               }
+               return $config;
+       }
+
+       /**
+        * Set (save) device client config.
+        *
+        * @param array $config config
+        * @return boolean true if config saved successfully, otherwise false
+        */
+       public function setConfig(array $config) {
+               $result = $this->writeConfig($config, self::CONFIG_FILE_PATH, self::CONFIG_FILE_FORMAT);
+               if ($result === true) {
+                       $this->config = null;
+               }
+               return $result;
+       }
+
+
+       /**
+        * Validate device config.
+        * Config validation should be used as early as config data is available.
+        * Validation is done in read/write config methods.
+        *
+        * @param string $section section name
+        * @param array $config config
+        * @return boolean true if config valid, otherwise false
+        */
+       private function validateConfig($section, array $config = []) {
+               $required_options = [$section => $this->required_options];
+               return $this->isConfigValid(
+                       $required_options,
+                       [$section => $config],
+                       self::CONFIG_FILE_FORMAT,
+                       self::CONFIG_FILE_PATH
+               );
+       }
+
+       public function getChangerDrives($changer) {
+               $drives = [];
+               $config = $this->getConfig($changer);
+               if (count($config) > 0) {
+                       $ach_drives = explode(',', $config['drives']);
+                       for ($i = 0; $i < count($ach_drives); $i++) {
+                               $drive = $this->getConfig($ach_drives[$i]);
+                               if (count($drive) > 0 && $drive['type'] === self::DEV_TYPE_DEVICE) {
+                                       $drive['name'] = $ach_drives[$i];
+                                       $drives[$drive['index']] = $drive;
+                               }
+                       }
+               }
+               return $drives;
+       }
+}
+?>
index 1ffc1393cb3b3eb4924765b3656c65f14f4fd147..69dcf599e78f5e414da2de977dc5524c6a37b0e3 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2021 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -175,5 +175,22 @@ LEFT JOIN Storage USING (StorageId)
                }
                return $volumes;
        }
+
+       /**
+        * Get volumes basing on specific criteria and return results as an array
+        * with volume names as keys.
+        *
+        * @param array $criteria array with criterias (@see VolumeManager::getVolumes)
+        * @param integer $limit_val limit results value
+        * @return array volume list with volume names as keys
+        */
+       public function getVolumesKeys($criteria = array(), $limit_val = 0) {
+               $volumes = [];
+               $vols = $this->getVolumes($criteria, $limit_val);
+               for ($i = 0; $i < count($vols); $i++) {
+                       $volumes[$vols[$i]->volumename] = $vols[$i];
+               }
+               return $volumes;
+       }
 }
 ?>
index 701f1bc3f7b3bde8e29ea2472d43989dd5fb5850..b3e13834449b16358c54e622643edd7319cca63b 100644 (file)
Binary files a/gui/baculum/protected/API/Lang/en/messages.mo and b/gui/baculum/protected/API/Lang/en/messages.mo differ
index 0ea4a2c074af0554c097ff6249d827a896062f17..05cbdd01c0f0eb9f779b3274f459e42badf91e24 100644 (file)
@@ -565,3 +565,75 @@ msgstr "Copy to clipboard"
 
 msgid "Logout"
 msgstr "Logout"
+
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Autochangers"
+msgstr "Autochangers"
+
+msgid "Add autochanger"
+msgstr "Add autochanger"
+
+msgid "Device"
+msgstr "Device"
+
+msgid "Name:"
+msgstr "Name:"
+
+msgid "Changer device:"
+msgstr "Changer device:"
+
+msgid "Changer command:"
+msgstr "Changer command:"
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Devices:"
+msgstr "Devices:"
+
+msgid "Add device"
+msgstr "Add device"
+
+msgid "Field cannot be empty."
+msgstr "Field cannot be empty."
+
+msgid "Invalid value."
+msgstr "Invalid value."
+
+msgid "Device with the given name already exists."
+msgstr "Device with the given name already exists."
+
+msgid "Edit device"
+msgstr "Edit device"
+
+msgid "Use CTRL + left-click to multiple item selection"
+msgstr "Use CTRL + left-click to multiple item selection"
+
+msgid "Device path:"
+msgstr "Device path:"
+
+msgid "Edit autochanger"
+msgstr "Edit autochanger"
+
+msgid "Autochanger"
+msgstr "Autochanger"
+
+msgid "Unable to delete device. Please unassign it from autochanger '%s' first."
+msgstr "Unable to delete device. Please unassign it from autochanger '%s' first."
+
+msgid "Autochanger with the given name already exists."
+msgstr "Autochanger with the given name already exists."
+
+msgid "Copy from Bacula SD config:"
+msgstr "Copy from Bacula SD config:"
+
+msgid "Changer command test:"
+msgstr "Changer command test:"
+
+msgid "Changer command error"
+msgstr "Changer command error"
index b05c80b8f63bac40e83bae72d1a1105dc531a0b3..f9414fcd3e84515c0eb7917413a9b70694cbedb4 100644 (file)
Binary files a/gui/baculum/protected/API/Lang/pl/messages.mo and b/gui/baculum/protected/API/Lang/pl/messages.mo differ
index e06c1c7f09563aa20054c0c25f31c961fb3b0b88..d7259ab09ad6db658040633996d9bb81889978a9 100644 (file)
@@ -572,3 +572,74 @@ msgstr "Kopiuj do schowka"
 msgid "Logout"
 msgstr "Wyloguj"
 
+msgid "Devices"
+msgstr "Urządzenia"
+
+msgid "Autochangers"
+msgstr "Zmieniarki taśm"
+
+msgid "Add autochanger"
+msgstr "Dodaj zmieniarkę taśm"
+
+msgid "Device"
+msgstr "Urządzenie"
+
+msgid "Name:"
+msgstr "Nazwa:"
+
+msgid "Changer device:"
+msgstr "Urządzenie zmieniarki taśm:"
+
+msgid "Changer command:"
+msgstr "Komenda zmieniarki taśm:"
+
+msgid "Drive index:"
+msgstr "Indeks napędu:"
+
+msgid "Drive index"
+msgstr "Indeks napędu"
+
+msgid "Devices:"
+msgstr "Urządzenia:"
+
+msgid "Add device"
+msgstr "Dodaj urządzenie"
+
+msgid "Field cannot be empty."
+msgstr "Pole nie może być puste."
+
+msgid "Invalid value."
+msgstr "Niepoprawna wartość."
+
+msgid "Device with the given name already exists."
+msgstr "Urządzenie o podanej nazwie już istnieje."
+
+msgid "Edit device"
+msgstr "Edytuj urządzenie"
+
+msgid "Use CTRL + left-click to multiple item selection"
+msgstr "Aby zaznaczyć wiele elementów użyj CTRL + lewy-klik"
+
+msgid "Device path:"
+msgstr "Ścieżka urządzenia:"
+
+msgid "Edit autochanger"
+msgstr "Edytuj zmieniarkę taśm"
+
+msgid "Autochanger"
+msgstr "Zmieniarka taśm"
+
+msgid "Unable to delete device. Please unassign it from autochanger '%s' first."
+msgstr "Nie można usunąć urządzenia. Proszę napierw wypisać go ze zmieniarki taśm '%s'."
+
+msgid "Autochanger with the given name already exists."
+msgstr "Zmieniarka taśm o podanej nazwie już istnieje."
+
+msgid "Copy from Bacula SD config:"
+msgstr "Kopiuj z konfiguracji Bacula SD:"
+
+msgid "Changer command test:"
+msgstr "Test komendy zmieniarki:"
+
+msgid "Changer command error"
+msgstr "Błąd komendy zmieniarki"
index 68ba5627681f6a8cf61e8ed29d735952c813d401..a64879fbeb19922a7979b65f08f2fce7f972df93 100644 (file)
Binary files a/gui/baculum/protected/API/Lang/pt/messages.mo and b/gui/baculum/protected/API/Lang/pt/messages.mo differ
index 5f895ba87ec5bc0def5125081a2c07aea44c511b..8cb00d291bf3670488294ca0b985cc4dae9751e2 100644 (file)
@@ -572,3 +572,74 @@ msgstr "Copiar para área de transferência"
 msgid "Logout"
 msgstr "Sair"
 
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Autochangers"
+msgstr "Autochangers"
+
+msgid "Add autochanger"
+msgstr "Add autochanger"
+
+msgid "Device"
+msgstr "Device"
+
+msgid "Name:"
+msgstr "Name:"
+
+msgid "Changer device:"
+msgstr "Changer device:"
+
+msgid "Changer command:"
+msgstr "Changer command:"
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Devices:"
+msgstr "Devices:"
+
+msgid "Add device"
+msgstr "Add device"
+
+msgid "Field cannot be empty."
+msgstr "Field cannot be empty."
+
+msgid "Invalid value."
+msgstr "Invalid value."
+
+msgid "Device with the given name already exists."
+msgstr "Device with the given name already exists."
+
+msgid "Edit device"
+msgstr "Edit device"
+
+msgid "Use CTRL + left-click to multiple item selection"
+msgstr "Use CTRL + left-click to multiple item selection"
+
+msgid "Device path:"
+msgstr "Device path:"
+
+msgid "Edit autochanger"
+msgstr "Edit autochanger"
+
+msgid "Autochanger"
+msgstr "Autochanger"
+
+msgid "Unable to delete device. Please unassign it from autochanger '%s' first."
+msgstr "Unable to delete device. Please unassign it from autochanger '%s' first."
+
+msgid "Autochanger with the given name already exists."
+msgstr "Autochanger with the given name already exists."
+
+msgid "Copy from Bacula SD config:"
+msgstr "Copy from Bacula SD config:"
+
+msgid "Changer command test:"
+msgstr "Changer command test:"
+
+msgid "Changer command error"
+msgstr "Changer command error"
index a983830d74c3dcf48405a9969f8dc0b054546a9b..97d51451dc2d31f9e16a17012a0538b4619b3f59 100644 (file)
Binary files a/gui/baculum/protected/API/Lang/ru/messages.mo and b/gui/baculum/protected/API/Lang/ru/messages.mo differ
index 26aae30f6168c4e4e9184f522f3981952eb0dfdd..c61cf7dfa55558739c83161f7dd4e15eae0e95df 100644 (file)
@@ -572,3 +572,74 @@ msgstr "Копировать в буфер обмена"
 msgid "Logout"
 msgstr "Выйти"
 
+msgid "Devices"
+msgstr "Devices"
+
+msgid "Autochangers"
+msgstr "Autochangers"
+
+msgid "Add autochanger"
+msgstr "Add autochanger"
+
+msgid "Device"
+msgstr "Device"
+
+msgid "Name:"
+msgstr "Name:"
+
+msgid "Changer device:"
+msgstr "Changer device:"
+
+msgid "Changer command:"
+msgstr "Changer command:"
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Devices:"
+msgstr "Devices:"
+
+msgid "Add device"
+msgstr "Add device"
+
+msgid "Field cannot be empty."
+msgstr "Field cannot be empty."
+
+msgid "Invalid value."
+msgstr "Invalid value."
+
+msgid "Device with the given name already exists."
+msgstr "Device with the given name already exists."
+
+msgid "Edit device"
+msgstr "Edit device"
+
+msgid "Use CTRL + left-click to multiple item selection"
+msgstr "Use CTRL + left-click to multiple item selection"
+
+msgid "Device path:"
+msgstr "Device path:"
+
+msgid "Edit autochanger"
+msgstr "Edit autochanger"
+
+msgid "Autochanger"
+msgstr "Autochanger"
+
+msgid "Unable to delete device. Please unassign it from autochanger '%s' first."
+msgstr "Unable to delete device. Please unassign it from autochanger '%s' first."
+
+msgid "Autochanger with the given name already exists."
+msgstr "Autochanger with the given name already exists."
+
+msgid "Copy from Bacula SD config:"
+msgstr "Copy from Bacula SD config:"
+
+msgid "Changer command test:"
+msgstr "Changer command test:"
+
+msgid "Changer command error"
+msgstr "Changer command error"
diff --git a/gui/baculum/protected/API/Pages/API/ChangerDriveLoad.php b/gui/baculum/protected/API/Pages/API/ChangerDriveLoad.php
new file mode 100644 (file)
index 0000000..7c5e073
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.API.Class.ChangerCommand');
+
+/**
+ * Load a given slot to autochanger drive.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerDriveLoad extends BaculumAPIServer {
+
+       public function get() {
+               $output = [];
+               $misc = $this->getModule('misc');
+               if ($this->Request->contains('out_id') && $misc->isValidAlphaNumeric($this->Request->itemAt('out_id'))) {
+                       $out_id = $this->Request->itemAt('out_id');
+                       $output = ChangerCommand::readOutputFile($out_id);
+               }
+               $this->output = $output;
+               $this->error = DeviceError::ERROR_NO_ERRORS;
+       }
+
+       public function set($id, $params) {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+               $drive = $this->Request->contains('drive') && $misc->isValidName($this->Request['drive']) ? $this->Request['drive'] : null;
+               $slot = $this->Request->contains('slot') && $misc->isValidInteger($this->Request['slot']) ? intval($this->Request['slot']) : null;
+
+               if (is_null($drive)) {
+                       $this->output = DeviceError::MSG_ERROR_DEVICE_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       $this->error = DeviceError::ERROR_DEVICE_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       return;
+               }
+
+               if (is_null($slot)) {
+                       $this->output = DeviceError::MSG_ERROR_DEVICE_WRONG_SLOT_NUMBER;
+                       $this->error = DeviceError::ERROR_DEVICE_WRONG_SLOT_NUMBER;
+                       return;
+               }
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'load',
+                       $drive,
+                       $slot,
+                       null,
+                       ChangerCommand::PTYPE_BG_CMD
+               );
+               $this->output = $result->output;
+               $this->error = $result->error;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Pages/API/ChangerDriveLoaded.php b/gui/baculum/protected/API/Pages/API/ChangerDriveLoaded.php
new file mode 100644 (file)
index 0000000..25d4760
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+/**
+ * Which slot is loaded in autochanger drive.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerDriveLoaded extends BaculumAPIServer {
+
+       public function get() {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+               $drive = $this->Request->contains('drive') && $misc->isValidName($this->Request['drive']) ? $this->Request['drive'] : null;
+
+               if (is_null($drive)) {
+                       $this->output = ChangerCommandError::MSG_ERROR_CHANGER_COMMAND_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       $this->error = ChangerCommandError::ERROR_CHANGER_COMMAND_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       return;
+               }
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'loaded',
+                       $drive
+               );
+
+               if ($result->error === 0 && count($result->output)) {
+                       $this->output = ['slot' => intval($result->output[0])];
+               } else {
+                       $this->output = $result->output;
+               }
+
+               $this->error = $result->error;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Pages/API/ChangerDriveUnload.php b/gui/baculum/protected/API/Pages/API/ChangerDriveUnload.php
new file mode 100644 (file)
index 0000000..84e9749
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.API.Class.ChangerCommand');
+
+/**
+ * Unload a given slot from autochanger drive.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerDriveUnload extends BaculumAPIServer {
+
+       public function get() {
+               $output = [];
+               $misc = $this->getModule('misc');
+               if ($this->Request->contains('out_id') && $misc->isValidAlphaNumeric($this->Request->itemAt('out_id'))) {
+                       $out_id = $this->Request->itemAt('out_id');
+                       $output = ChangerCommand::readOutputFile($out_id);
+               }
+               $this->output = $output;
+               $this->error = DeviceError::ERROR_NO_ERRORS;
+       }
+
+       public function set($id, $params) {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+               $drive = $this->Request->contains('drive') && $misc->isValidName($this->Request['drive']) ? $this->Request['drive'] : null;
+               $slot = $this->Request->contains('slot') && $misc->isValidInteger($this->Request['slot']) ? intval($this->Request['slot']) : null;
+
+               if (is_null($drive)) {
+                       $this->output = ChangerCommandError::MSG_ERROR_CHANGER_COMMAND_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       $this->error = ChangerCommandError::ERROR_CHANGER_COMMAND_AUTOCHANGER_DRIVE_DOES_NOT_EXIST;
+                       return;
+               }
+
+               if (is_null($slot)) {
+                       $this->output = ChangerCommandError::MSG_ERROR_CHANGER_COMMAND_WRONG_SLOT_NUMBER;
+                       $this->error = ChangerCommandError::ERROR_CHANGER_COMMAND_WRONG_SLOT_NUMBER;
+                       return;
+               }
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'unload',
+                       $drive,
+                       $slot,
+                       null,
+                       ChangerCommand::PTYPE_BG_CMD
+               );
+               $this->output = $result->output;
+               $this->error = $result->error;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Pages/API/ChangerList.php b/gui/baculum/protected/API/Pages/API/ChangerList.php
new file mode 100644 (file)
index 0000000..74c9b91
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+/**
+ * List autochanger volume names (requires barcode reader).
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerList extends BaculumAPIServer {
+
+       const LIST_PATTERN = '/^(?P<slot>\d+):(?P<volume>\S+)$/';
+
+       public function get() {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+
+               if (is_null($device_name)) {
+                       $output = DeviceError::MSG_ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST;
+                       $error = DeviceError::ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST;
+                       return;
+               }
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'list'
+               );
+
+               if ($result->error === 0) {
+                       $this->output = $this->parseList($result->output);
+               } else {
+                       $this->output = $result->output;
+               }
+               $this->error = $result->error;
+       }
+
+       private function parseList($output) {
+               $list = [];
+               for ($i = 0; $i < count($output); $i++) {
+                       if (preg_match(self::LIST_PATTERN, $output[$i], $match) == 1) {
+                               $list[] = [
+                                       'slot' => $match['slot'],
+                                       'volume' => $match['volume']
+                               ];
+                       }
+               }
+               return $list;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Pages/API/ChangerListAll.php b/gui/baculum/protected/API/Pages/API/ChangerListAll.php
new file mode 100644 (file)
index 0000000..876aea3
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+/**
+ * Autochanger list all slots.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerListAll extends BaculumAPIServer {
+
+       const LIST_ALL_DRIVE_PATTERN = '/^D:(?P<index>\d+):(?P<state>[EF]):?(?P<slot>\d+)?:?(?P<volume>\S+)?$/';
+       const LIST_ALL_SLOT_PATTERN = '/^S:(?P<slot>\d+):(?P<state>[EF]):?(?P<volume>\S+)?$/';
+       const LIST_ALL_IO_SLOT_PATTERN = '/^I:(?P<slot>\d+):(?P<state>[EF]):?(?P<volume>\S+)?$/';
+
+       public function get() {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+
+               if (is_null($device_name)) {
+                       $output = DeviceError::MSG_ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST;
+                       $error = DeviceError::ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST;
+                       return;
+               }
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'listall'
+               );
+
+               if ($result->error === 0) {
+                       $this->output = $this->parseListAll($device_name, $result->output);
+               } else {
+                       $this->output = $result->output;
+               }
+               $this->error = $result->error;
+       }
+
+       private function parseListAll($device_name, $output) {
+               $list = ['drives' => [], 'slots' => [], 'ie_slots' => []];
+               $drives = $this->getModule('device_config')->getChangerDrives($device_name);
+               $volumes = $this->getModule('volume')->getVolumesKeys();
+               $get_volume_info  = function($volname) use ($volumes) {
+                       $volume = [
+                               'mediaid' => 0,
+                               'volume' => '',
+                               'mediatype' => '',
+                               'pool' => '',
+                               'lastwritten' => '',
+                               'whenexpire' => '',
+                               'volbytes' => '',
+                               'volstatus' => '',
+                               'slot' => ''
+                       ];
+                       if (key_exists($volname, $volumes)) {
+                               $volume['mediaid'] = intval($volumes[$volname]->mediaid);
+                               $volume['mediatype'] = $volumes[$volname]->mediatype;
+                               $volume['pool'] = $volumes[$volname]->pool;
+                               $volume['lastwritten'] = $volumes[$volname]->lastwritten;
+                               $volume['whenexpire'] = $volumes[$volname]->whenexpire;
+                               $volume['volbytes'] = $volumes[$volname]->volbytes;
+                               $volume['volstatus'] = $volumes[$volname]->volstatus;
+                               $volume['slot'] = $volumes[$volname]->slot;
+                       }
+                       return $volume;
+               };
+               for ($i = 0; $i < count($output); $i++) {
+                       if (preg_match(self::LIST_ALL_DRIVE_PATTERN, $output[$i], $match) == 1) {
+                               $index = intval($match['index']);
+                               if (!key_exists($index, $drives)) {
+                                       continue;
+                               }
+                               $drive = $drives[$index]['name'];
+                               $device = $drives[$index]['device'];
+                               $volume = '';
+                               if (key_exists('volume', $match)) {
+                                       $volume = $match['volume'];
+                               }
+                               $volinfo = $get_volume_info($volume);
+                               $list['drives'][] = [
+                                       'type' => 'drive',
+                                       'index' => $index,
+                                       'drive' => $drive,
+                                       'device' => $device,
+                                       'state' => $match['state'],
+                                       'slot_ach' => key_exists('slot', $match) ? intval($match['slot']) : '',
+                                       'mediaid' => $volinfo['mediaid'],
+                                       'volume' => $volume,
+                                       'mediatype' => $volinfo['mediatype'],
+                                       'pool' => $volinfo['pool'],
+                                       'lastwritten' => $volinfo['lastwritten'],
+                                       'whenexpire' => $volinfo['whenexpire'],
+                                       'volbytes' => $volinfo['volbytes'],
+                                       'volstatus' => $volinfo['volstatus'],
+                                       'slot_cat' => $volinfo['slot']
+                               ];
+                       } elseif (preg_match(self::LIST_ALL_SLOT_PATTERN, $output[$i], $match) == 1) {
+                               $volume = '';
+                               if (key_exists('volume', $match)) {
+                                       $volume = $match['volume'];
+                               }
+                               $volinfo = $get_volume_info($volume);
+                               $list['slots'][] = [
+                                       'type' => 'slot',
+                                       'slot_ach' => intval($match['slot']),
+                                       'state' => $match['state'],
+                                       'mediaid' => $volinfo['mediaid'],
+                                       'volume' => $volume,
+                                       'mediatype' => $volinfo['mediatype'],
+                                       'pool' => $volinfo['pool'],
+                                       'lastwritten' => $volinfo['lastwritten'],
+                                       'whenexpire' => $volinfo['whenexpire'],
+                                       'volbytes' => $volinfo['volbytes'],
+                                       'volstatus' => $volinfo['volstatus'],
+                                       'slot_cat' => $volinfo['slot']
+                               ];
+                       } elseif (preg_match(self::LIST_ALL_IO_SLOT_PATTERN, $output[$i], $match) == 1) {
+                               $volume = '';
+                               if (key_exists('volume', $match)) {
+                                       $volume = $match['volume'];
+                               }
+                               $volinfo = $get_volume_info($volume);
+                               $list['ie_slots'][] = [
+                                       'type' => 'ie_slot',
+                                       'slot_ach' => intval($match['slot']),
+                                       'state' => $match['state'],
+                                       'mediaid' => $volinfo['mediaid'],
+                                       'volume' => $volume,
+                                       'mediatype' => $volinfo['mediatype'],
+                                       'pool' => $volinfo['pool'],
+                                       'lastwritten' => $volinfo['lastwritten'],
+                                       'whenexpire' => $volinfo['whenexpire'],
+                                       'volbytes' => $volinfo['volbytes'],
+                                       'volstatus' => $volinfo['volstatus'],
+                                       'slot_cat' => $volinfo['slot']
+                               ];
+                       }
+               }
+               return $list;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Pages/API/ChangerSlots.php b/gui/baculum/protected/API/Pages/API/ChangerSlots.php
new file mode 100644 (file)
index 0000000..ba741d3
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+/**
+ * How many slots has autochanger.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerSlots extends BaculumAPIServer {
+
+       public function get() {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'slots'
+               );
+
+               if ($result->error === 0 && count($result->output)) {
+                       $this->output = ['slots' => intval($result->output[0])];
+               } else {
+                       $this->output = $result->output;
+               }
+               $this->error = $result->error;
+       }
+}
+?>
diff --git a/gui/baculum/protected/API/Pages/API/ChangerSlotsTransfer.php b/gui/baculum/protected/API/Pages/API/ChangerSlotsTransfer.php
new file mode 100644 (file)
index 0000000..5c0d7c6
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.API.Class.ChangerCommand');
+
+/**
+ * Transfer autochanger tape from slot to slot.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class ChangerSlotsTransfer extends BaculumAPIServer {
+
+       public function get() {
+               $output = [];
+               $misc = $this->getModule('misc');
+               if ($this->Request->contains('out_id') && $misc->isValidAlphaNumeric($this->Request->itemAt('out_id'))) {
+                       $out_id = $this->Request->itemAt('out_id');
+                       $output = ChangerCommand::readOutputFile($out_id);
+               }
+               $this->output = $output;
+               $this->error = DeviceError::ERROR_NO_ERRORS;
+       }
+
+       public function set($id, $params) {
+               $misc = $this->getModule('misc');
+               $device_name = $this->Request->contains('device_name') && $misc->isValidName($this->Request['device_name']) ? $this->Request['device_name'] : null;
+               $slotsrc = $this->Request->contains('slotsrc') && $misc->isValidInteger($this->Request['slotsrc']) ? intval($this->Request['slotsrc']) : null;
+               $slotdest = $this->Request->contains('slotdest') && $misc->isValidInteger($this->Request['slotdest']) ? intval($this->Request['slotdest']) : null;
+
+               if (is_null($slotsrc) || is_null($slotdest)) {
+                       $this->output = DeviceError::MSG_ERROR_DEVICE_WRONG_SLOT_NUMBER;
+                       $this->error = DeviceError::ERROR_DEVICE_WRONG_SLOT_NUMBER;
+                       return;
+               }
+
+               $result = $this->getModule('changer_command')->execChangerCommand(
+                       $device_name,
+                       'transfer',
+                       null,
+                       $slotsrc,
+                       $slotdest,
+                       ChangerCommand::PTYPE_BG_CMD
+               );
+               $this->output = $result->output;
+               $this->error = $result->error;
+       }
+}
+?>
index 489eff055bf7c300b6439d8427d3aef20ef5515d..8a04dc8623ab2d80396df8a5336295f8f93a9588 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2021 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -20,6 +20,8 @@
  * Bacula(R) is a registered trademark of Kern Sibbald.
  */
  
+Prado::using('Application.API.Class.Bconsole');
+
 /**
  * Mount storage command endpoint.
  *
  * @package Baculum API
  */
 class StorageMount extends BaculumAPIServer {
+
        public function get() {
-               $storageid = $this->Request->contains('id') ? intval($this->Request['id']) : 0;
+               $output = [];
+               $misc = $this->getModule('misc');
+               if ($this->Request->contains('out_id') && $misc->isValidAlphaNumeric($this->Request->itemAt('out_id'))) {
+                       $out_id = $this->Request->itemAt('out_id');
+                       $output = Bconsole::readOutputFile($out_id);
+               }
+               $this->output = $output;
+               $this->error = StorageError::ERROR_NO_ERRORS;
+       }
+
+       public function set($id, $params) {
                $drive = $this->Request->contains('drive') ? intval($this->Request['drive']) : 0;
                $device = ($this->Request->contains('device') && $this->getModule('misc')->isValidName($this->Request['device'])) ? $this->Request['device'] : null;
                $slot = $this->Request->contains('slot') ? intval($this->Request['slot']) : 0;
 
                $result = $this->getModule('bconsole')->bconsoleCommand(
                        $this->director,
-                       array('.storage')
+                       ['.storage']
                );
                if ($result->exitcode === 0) {
                        array_shift($result->output);
-                       $storage = $this->getModule('storage')->getStorageById($storageid);
+                       $storage = $this->getModule('storage')->getStorageById($id);
                        if (is_object($storage) && in_array($storage->name, $result->output)) {
                                $result = $this->getModule('bconsole')->bconsoleCommand(
                                        $this->director,
-                                       array(
+                                       [
                                                'mount',
                                                'storage="' . $storage->name . '"',
                                                (is_string($device) ? 'device="' . $device . '" drive=0' : 'drive=' . $drive),
                                                'slot=' . $slot
-                                       )
+                                       ],
+                                       Bconsole::PTYPE_BG_CMD,
+                                       true
                                );
                                $this->output = $result->output;
                                $this->error = $result->exitcode;
diff --git a/gui/baculum/protected/API/Pages/API/StorageMountV1.php b/gui/baculum/protected/API/Pages/API/StorageMountV1.php
new file mode 100644 (file)
index 0000000..f98f3fa
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+/**
+ * Mount storage command endpoint.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class StorageMountV1 extends BaculumAPIServer {
+       public function get() {
+               $storageid = $this->Request->contains('id') ? intval($this->Request['id']) : 0;
+               $drive = $this->Request->contains('drive') ? intval($this->Request['drive']) : 0;
+               $device = ($this->Request->contains('device') && $this->getModule('misc')->isValidName($this->Request['device'])) ? $this->Request['device'] : null;
+               $slot = $this->Request->contains('slot') ? intval($this->Request['slot']) : 0;
+
+               $result = $this->getModule('bconsole')->bconsoleCommand(
+                       $this->director,
+                       array('.storage')
+               );
+               if ($result->exitcode === 0) {
+                       array_shift($result->output);
+                       $storage = $this->getModule('storage')->getStorageById($storageid);
+                       if (is_object($storage) && in_array($storage->name, $result->output)) {
+                               $result = $this->getModule('bconsole')->bconsoleCommand(
+                                       $this->director,
+                                       array(
+                                               'mount',
+                                               'storage="' . $storage->name . '"',
+                                               (is_string($device) ? 'device="' . $device . '" drive=0' : 'drive=' . $drive),
+                                               'slot=' . $slot
+                                       )
+                               );
+                               $this->output = $result->output;
+                               $this->error = $result->exitcode;
+                       } else {
+                               $this->output = StorageError::MSG_ERROR_STORAGE_DOES_NOT_EXISTS;
+                               $this->error = StorageError::ERROR_STORAGE_DOES_NOT_EXISTS;
+                       }
+               } else {
+                       $this->output = $result->output;
+                       $this->error = $result->exitcode;
+               }
+       }
+}
+
+?>
index ce15cdbc8dd38da4beec18a2a0a07b4f37c7564e..965aa5f6d48dc46c3db756d71117208252726080 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2021 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -20,6 +20,8 @@
  * Bacula(R) is a registered trademark of Kern Sibbald.
  */
  
+Prado::using('Application.API.Class.Bconsole');
+
 /**
  * Release storage command endpoint.
  *
  * @package Baculum API
  */
 class StorageRelease extends BaculumAPIServer {
+
        public function get() {
-               $storageid = $this->Request->contains('id') ? intval($this->Request['id']) : 0;
+               $output = [];
+               $misc = $this->getModule('misc');
+               if ($this->Request->contains('out_id') && $misc->isValidAlphaNumeric($this->Request->itemAt('out_id'))) {
+                       $out_id = $this->Request->itemAt('out_id');
+                       $output = Bconsole::readOutputFile($out_id);
+               }
+               $this->output = $output;
+               $this->error = StorageError::ERROR_NO_ERRORS;
+       }
+
+       public function set($id, $params) {
                $drive = $this->Request->contains('drive') ? intval($this->Request['drive']) : 0;
                $device = $this->Request->contains('device') ? $this->Request['device'] : null;
 
                $result = $this->getModule('bconsole')->bconsoleCommand(
                        $this->director,
-                       array('.storage')
+                       ['.storage']
                );
-
                if ($result->exitcode === 0) {
                        array_shift($result->output);
-                       $storage = $this->getModule('storage')->getStorageById($storageid);
+                       $storage = $this->getModule('storage')->getStorageById($id);
                        if (is_object($storage) && in_array($storage->name, $result->output)) {
                                $result = $this->getModule('bconsole')->bconsoleCommand(
                                        $this->director,
-                                       array(
+                                       [
                                                'release',
                                                'storage="' . $storage->name . '"',
                                                (is_string($device) ? 'device="' . $device . '" drive=0 slot=0' : 'drive=' . $drive . ' slot=0')
-                                       )
+                                       ],
+                                       Bconsole::PTYPE_BG_CMD,
+                                       true
                                );
                                $this->output = $result->output;
                                $this->error = $result->exitcode;
diff --git a/gui/baculum/protected/API/Pages/API/StorageReleaseV1.php b/gui/baculum/protected/API/Pages/API/StorageReleaseV1.php
new file mode 100644 (file)
index 0000000..e319f75
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+/**
+ * Release storage command endpoint.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class StorageReleaseV1 extends BaculumAPIServer {
+       public function get() {
+               $storageid = $this->Request->contains('id') ? intval($this->Request['id']) : 0;
+               $drive = $this->Request->contains('drive') ? intval($this->Request['drive']) : 0;
+               $device = $this->Request->contains('device') ? $this->Request['device'] : null;
+
+               $result = $this->getModule('bconsole')->bconsoleCommand(
+                       $this->director,
+                       array('.storage')
+               );
+
+               if ($result->exitcode === 0) {
+                       array_shift($result->output);
+                       $storage = $this->getModule('storage')->getStorageById($storageid);
+                       if (is_object($storage) && in_array($storage->name, $result->output)) {
+                               $result = $this->getModule('bconsole')->bconsoleCommand(
+                                       $this->director,
+                                       array(
+                                               'release',
+                                               'storage="' . $storage->name . '"',
+                                               (is_string($device) ? 'device="' . $device . '" drive=0 slot=0' : 'drive=' . $drive . ' slot=0')
+                                       )
+                               );
+                               $this->output = $result->output;
+                               $this->error = $result->exitcode;
+                       } else {
+                               $this->output = StorageError::MSG_ERROR_STORAGE_DOES_NOT_EXISTS;
+                               $this->error = StorageError::ERROR_STORAGE_DOES_NOT_EXISTS;
+                       }
+               } else {
+                       $this->output = $result->output;
+                       $this->error = $result->exitcode;
+               }
+       }
+}
+
+?>
index 657f1fbe82fccbef3b41200618b6bb36ca6ced07..e89f6a176a3f2698db645eb1c42f52fedde71906 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2021 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -19,6 +19,8 @@
  *
  * Bacula(R) is a registered trademark of Kern Sibbald.
  */
+
+Prado::using('Application.API.Class.Bconsole');
  
 /**
  * Storage umount command endpoint.
  * @package Baculum API
  */
 class StorageUmount extends BaculumAPIServer {
+
        public function get() {
-               $storageid = $this->Request->contains('id') ? intval($this->Request['id']) : 0;
+               $output = [];
+               $misc = $this->getModule('misc');
+               if ($this->Request->contains('out_id') && $misc->isValidAlphaNumeric($this->Request->itemAt('out_id'))) {
+                       $out_id = $this->Request->itemAt('out_id');
+                       $output = Bconsole::readOutputFile($out_id);
+               }
+               $this->output = $output;
+               $this->error = StorageError::ERROR_NO_ERRORS;
+       }
+
+       public function set($id, $params) {
                $drive = $this->Request->contains('drive') ? intval($this->Request['drive']) : 0;
                $device = $this->Request->contains('device') ? $this->Request['device'] : null;
 
                $result = $this->getModule('bconsole')->bconsoleCommand(
                        $this->director,
-                       array('.storage')
+                       ['.storage']
                );
                if ($result->exitcode === 0) {
                        array_shift($result->output);
-                       $storage = $this->getModule('storage')->getStorageById($storageid);
+                       $storage = $this->getModule('storage')->getStorageById($id);
                        if (is_object($storage) && in_array($storage->name, $result->output)) {
                                $result = $this->getModule('bconsole')->bconsoleCommand(
                                        $this->director,
-                                       array(
+                                       [
                                                'umount',
                                                'storage="' . $storage->name . '"',
                                                (is_string($device) ? 'device="' . $device . '" slot=0 drive=0' : 'drive=' . $drive . ' slot=0')
-                                       )
+                                       ],
+                                       Bconsole::PTYPE_BG_CMD,
+                                       true
                                );
                                $this->output = $result->output;
                                $this->error = $result->exitcode;
diff --git a/gui/baculum/protected/API/Pages/API/StorageUmountV1.php b/gui/baculum/protected/API/Pages/API/StorageUmountV1.php
new file mode 100644 (file)
index 0000000..705094b
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+/**
+ * Storage umount command endpoint.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category API
+ * @package Baculum API
+ */
+class StorageUmountV1 extends BaculumAPIServer {
+       public function get() {
+               $storageid = $this->Request->contains('id') ? intval($this->Request['id']) : 0;
+               $drive = $this->Request->contains('drive') ? intval($this->Request['drive']) : 0;
+               $device = $this->Request->contains('device') ? $this->Request['device'] : null;
+
+               $result = $this->getModule('bconsole')->bconsoleCommand(
+                       $this->director,
+                       array('.storage')
+               );
+               if ($result->exitcode === 0) {
+                       array_shift($result->output);
+                       $storage = $this->getModule('storage')->getStorageById($storageid);
+                       if (is_object($storage) && in_array($storage->name, $result->output)) {
+                               $result = $this->getModule('bconsole')->bconsoleCommand(
+                                       $this->director,
+                                       array(
+                                               'umount',
+                                               'storage="' . $storage->name . '"',
+                                               (is_string($device) ? 'device="' . $device . '" slot=0 drive=0' : 'drive=' . $drive . ' slot=0')
+                                       )
+                               );
+                               $this->output = $result->output;
+                               $this->error = $result->exitcode;
+                       } else {
+                               $this->output = StorageError::MSG_ERROR_STORAGE_DOES_NOT_EXISTS;
+                               $this->error = StorageError::ERROR_STORAGE_DOES_NOT_EXISTS;
+                       }
+               } else {
+                       $this->output = $result->output;
+                       $this->error = $result->exitcode;
+               }
+       }
+}
+
+?>
index 5a943057b268e9cfd57dc7072108741d03811da1..988f47f8a9ca706f8c6646d4e444b5c4c10f369f 100644 (file)
@@ -49,14 +49,14 @@ class VolumeLabel extends BaculumAPIServer {
                $misc = $this->getModule('misc');
 
                if (!$misc->isValidName($volume)) {
-                       $this->output = VolumeError::ERROR_INVALID_VOLUME;
-                       $this->error = VolumeError::MSG_ERROR_INVALID_VOLUME;
+                       $this->output = VolumeError::MSG_ERROR_INVALID_VOLUME;
+                       $this->error = VolumeError::ERROR_INVALID_VOLUME;
                        return;
                }
 
                if (!$misc->isValidInteger($slot)) {
-                       $this->output = VolumeError::ERROR_INVALID_SLOT;
-                       $this->error = VolumeError::MSG_ERROR_INVALID_SLOT;
+                       $this->output = VolumeError::MSG_ERROR_INVALID_SLOT;
+                       $this->error = VolumeError::ERROR_INVALID_SLOT;
                        return;
                }
 
index 6381b73f5328563ee7020882a0888123a64e0dbd..568c92f61e2ce418c41f5c3dfcecf91a45310655 100644 (file)
@@ -33,6 +33,7 @@
                <module id="api_config" class="Application.API.Class.APIConfig" />
                <module id="bacula_config" class="Application.API.Class.BaculaConfig" />
                <module id="bacula_setting" class="Application.API.Class.BaculaSetting" />
+               <module id="device_config" class="Application.API.Class.DeviceConfig" />
                <!-- logging modules -->
                <module id="log" class="System.Util.TLogRouter">
                        <route class="TFileLogRoute"  Categories="Execute, External, Application, General, Security" LogPath="Application.API.Logs" LogFile="baculum-api.log" MaxFileSize="1000" MaxLogFiles="5" />
@@ -46,5 +47,7 @@
                <!-- bconsole command modules -->
                <module id="ls" class="Application.API.Class.Ls" />
                <module id="list" class="Application.API.Class.BList" />
+               <!-- changer command modules -->
+               <module id="changer_command" class="Application.API.Class.ChangerCommand" />
        </modules>
 </configuration>
index bccbc1e1972561452097ccb136069b714d136bbe..5f3bbc43376e9845b6517f168323abff5b7ccda9 100644 (file)
        <url ServiceParameter="StorageMount" pattern="api/v2/storages/{id}/mount/" parameters.id="\d+" />
        <url ServiceParameter="StorageUmount" pattern="api/v2/storages/{id}/umount/" parameters.id="\d+" />
        <url ServiceParameter="StorageRelease" pattern="api/v2/storages/{id}/release/" parameters.id="\d+" />
+       <!-- devices endpoints -->
+       <url ServiceParameter="ChangerDriveLoad" pattern="api/v2/devices/{device_name}/load/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
+       <url ServiceParameter="ChangerDriveUnload" pattern="api/v2/devices/{device_name}/unload/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
+       <url ServiceParameter="ChangerDriveLoaded" pattern="api/v2/devices/{device_name}/loaded/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
+       <url ServiceParameter="ChangerList" pattern="api/v2/devices/{device_name}/list/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
+       <url ServiceParameter="ChangerListAll" pattern="api/v2/devices/{device_name}/listall/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
+       <url ServiceParameter="ChangerSlots" pattern="api/v2/devices/{device_name}/slots/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
+       <url ServiceParameter="ChangerSlotsTransfer" pattern="api/v2/devices/{device_name}/transfer/" parameters.device_name="[a-zA-Z0-9:.\-_ ]+" />
        <!-- volumes (media) endpoints-->
        <url ServiceParameter="Volumes" pattern="api/v2/volumes/" />
        <url ServiceParameter="Volume" pattern="api/v2/volumes/{id}/" parameters.id="\d+" />
        <url ServiceParameter="StoragesShow" pattern="api/v1/storages/show/" />
        <url ServiceParameter="StorageShow" pattern="api/v1/storages/{id}/show/" parameters.id="\d+" />
        <url ServiceParameter="StorageStatus" pattern="api/v1/storages/{id}/status/" parameters.id="\d+" />
-       <url ServiceParameter="StorageMount" pattern="api/v1/storages/{id}/mount/" parameters.id="\d+" />
-       <url ServiceParameter="StorageUmount" pattern="api/v1/storages/{id}/umount/" parameters.id="\d+" />
-       <url ServiceParameter="StorageRelease" pattern="api/v1/storages/{id}/release/" parameters.id="\d+" />
+       <url ServiceParameter="StorageMountV1" pattern="api/v1/storages/{id}/mount/" parameters.id="\d+" />
+       <url ServiceParameter="StorageUmountV1" pattern="api/v1/storages/{id}/umount/" parameters.id="\d+" />
+       <url ServiceParameter="StorageReleaseV1" pattern="api/v1/storages/{id}/release/" parameters.id="\d+" />
        <!-- volumes (media) endpoints-->
        <url ServiceParameter="Volumes" pattern="api/v1/volumes/" />
        <url ServiceParameter="Volume" pattern="api/v1/volumes/{id}/" parameters.id="\d+" />
diff --git a/gui/baculum/protected/API/Pages/Panel/APIDevices.page b/gui/baculum/protected/API/Pages/Panel/APIDevices.page
new file mode 100644 (file)
index 0000000..eb1d4e3
--- /dev/null
@@ -0,0 +1,866 @@
+<%@ MasterClass="Application.API.Layouts.Main" Theme="Baculum-v2"%>
+<com:TContent ID="Main">
+       <header class="w3-container w3-block">
+               <h5>
+                       <i class="fas fa-user-shield"></i> <%[ Devices ]%>
+               </h5>
+       </header>
+       <div class="w3-bar w3-green w3-margin-bottom">
+               <button type="button" id="btn_devices_autochanger" class="w3-bar-item w3-button tab_btn w3-grey" onclick="W3Tabs.open(this.id, 'devices_autochanger');"><%[ Autochangers ]%></button>
+               <button type="button" id="btn_devices_device" class="w3-bar-item w3-button tab_btn" onclick="W3Tabs.open(this.id, 'devices_device'); oDeviceList.table.responsive.recalc();"><%[ Devices ]%></button>
+       </div>
+       <div class="w3-container tab_item" id="devices_autochanger">
+               <a href="javascript:void(0)" class="w3-button w3-green w3-margin-bottom" onclick="oAPIAutochangers.add_autochanger();">
+                       <i class="fas fa-plus"></i> &nbsp;<%[ Add autochanger ]%>
+               </a>
+               <table id="autochanger_list" class="w3-table w3-striped w3-hoverable w3-white w3-margin-bottom" style="width: 100%">
+                       <thead>
+                               <tr>
+                                       <th></th>
+                                       <th><%[ Name ]%></th>
+                                       <th><%[ Device ]%></th>
+                                       <th><%[ Actions ]%></th>
+                               </tr>
+                       </thead>
+                       <tbody id="autochanger_list_body"></tbody>
+                       <tfoot>
+                               <tr>
+                                       <th></th>
+                                       <th><%[ Name ]%></th>
+                                       <th><%[ Device ]%></th>
+                                       <th><%[ Actions ]%></th>
+                               </tr>
+                       </tfoot>
+               </table>
+       </div>
+       <com:Application.API.Portlets.SudoConfig ID="SudoConfig" />
+<script>
+var oAutochangerList = {
+       ids: {
+               autochanger_list: 'autochanger_list',
+               autochanger_list_body: 'autochanger_list_body'
+       },
+       table: null,
+       data: [],
+       init: function() {
+               if (!this.table) {
+                       this.set_table();
+               } else {
+                       var page = this.table.page();
+                       this.table.clear().rows.add(this.data).draw();
+                       this.table.page(page).draw(false);
+               }
+       },
+       set_table: function() {
+               this.table = $('#' + this.ids.autochanger_list).DataTable({
+                       data: this.data,
+                       deferRender: true,
+                       dom: 'lBfrtip',
+                       stateSave: true,
+                       buttons: [
+                               'copy', 'csv', 'colvis'
+                       ],
+                       columns: [
+                               {
+                                       className: 'details-control',
+                                       orderable: false,
+                                       data: null,
+                                       defaultContent: '<button type="button" class="w3-button w3-blue"><i class="fa fa-angle-down"></i></button>'
+                               },
+                               {data: 'name'},
+                               {data: 'device'},
+                               {
+                                       data: 'name',
+                                       render: function(data, type, row) {
+                                               var span = document.createElement('SPAN');
+                                               span.className = 'w3-right';
+
+                                               var chpwd_btn = document.createElement('BUTTON');
+                                               chpwd_btn.className = 'w3-button w3-green';
+                                               chpwd_btn.type = 'button';
+                                               var i = document.createElement('I');
+                                               i.className = 'fas fa-edit';
+                                               var label = document.createTextNode(' <%[ Edit ]%>');
+                                               chpwd_btn.appendChild(i);
+                                               chpwd_btn.innerHTML += '&nbsp';
+                                               chpwd_btn.appendChild(label);
+                                               chpwd_btn.setAttribute('onclick', 'oAPIAutochangers.edit_autochanger("' + data + '")');
+
+                                               var del_btn = document.createElement('BUTTON');
+                                               del_btn.className = 'w3-button w3-red w3-margin-left';
+                                               del_btn.type = 'button';
+                                               var i = document.createElement('I');
+                                               i.className = 'fas fa-trash-alt';
+                                               var label = document.createTextNode(' <%[ Delete ]%>');
+                                               del_btn.appendChild(i);
+                                               del_btn.innerHTML += '&nbsp';
+                                               del_btn.appendChild(label);
+                                               del_btn.setAttribute('onclick', 'oAPIAutochangers.delete_autochanger("' + data + '")');
+
+                                               span.appendChild(chpwd_btn);
+                                               span.appendChild(del_btn);
+
+                                               return span.outerHTML;
+                                       }
+                               }
+                       ],
+                       responsive: {
+                               details: {
+                                       type: 'column'
+                               }
+                       },
+                       columnDefs: [{
+                               className: 'control',
+                               orderable: false,
+                               targets: 0
+                       },
+                       {
+                               className: "dt-center",
+                               targets: [ 3 ]
+                       }],
+                       order: [1, 'asc'],
+               });
+       }
+};
+var bin_fields = {
+       changer_command: [
+               '<%=$this->ChangerCommand->ClientID%>'
+       ]
+};
+var bin_opts = {
+       changer_command: {
+               base_path: true
+       }
+};
+oSudoConfig.set_bin_fields(bin_fields);
+oSudoConfig.set_bin_opts(bin_opts);
+</script>
+       <div id="autochanger_window" class="w3-modal">
+               <div class="w3-modal-content w3-animate-top w3-card-4">
+                       <header class="w3-container w3-teal">
+                               <span onclick="document.getElementById('autochanger_window').style.display = 'none';" class="w3-button w3-display-topright">&times;</span>
+                               <h2 id="autochanger_window_title_add" style="display: none"><%[ Add autochanger ]%></h2>
+                               <h2 id="autochanger_window_title_edit" style="display: none"><%[ Edit autochanger ]%></h2>
+                       </header>
+                       <div class="w3-container w3-margin-left w3-margin-right w3-text-teal">
+                               <span id="autochanger_exists" class="error" style="display: none"><ul><li><%[ Autochanger with the given name already exists. ]%></li></ul></span>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><label for="autochanger_config"><%[ Copy from Bacula SD config: ]%></label></div>
+                                       <div class="w3-half">
+                                               <select id="autochanger_config" class="w3-select w3-border"></select>
+                                       </div>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="AutochangerName" Text="<%[ Name: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveTextBox
+                                                       ID="AutochangerName"
+                                                       AutoPostBack="false"
+                                                       MaxLength="100"
+                                                       CssClass="w3-input w3-border"
+                                                       Attributes.placeholder="My Autochanger 123"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="AutchangerGroup"
+                                                       ControlToValidate="AutochangerName"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                               <com:TRegularExpressionValidator
+                                                       ValidationGroup="AutchangerGroup"
+                                                       RegularExpression="<%=DeviceConfig::DEVICE_PATH_PATTERN%>"
+                                                       ControlToValidate="AutochangerName"
+                                                       ErrorMessage="<%[ Invalid value. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="ChangerDevice" Text="<%[ Changer device: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveTextBox
+                                                       ID="ChangerDevice"
+                                                       AutoPostBack="false"
+                                                       MaxLength="100"
+                                                       CssClass="w3-input w3-border"
+                                                       Attributes.placeholder="/dev/sg4"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="AutchangerGroup"
+                                                       ControlToValidate="ChangerDevice"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="ChangerCommand" Text="<%[ Changer command: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveTextBox
+                                                       ID="ChangerCommand"
+                                                       AutoPostBack="false"
+                                                       MaxLength="100"
+                                                       CssClass="w3-input w3-border"
+                                                       Attributes.placeholder="/some/path/mtx-changer %c %o %S %a %d"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="AutchangerGroup"
+                                                       ControlToValidate="ChangerCommand"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third">
+                                               <com:TLabel
+                                                       ForControl="ChangerCommandUseSudo"
+                                                       Text="<%[ Use sudo: ]%>"
+                                               />
+                                       </div>
+                                       <div class="w3-col w3-half">
+                                               <com:TActiveCheckBox
+                                                       ID="ChangerCommandUseSudo"
+                                                       CssClass="w3-check"
+                                               /> &nbsp;<a href="javascript:void(0)" onclick="oSudoConfig.get_config('changer_command');"><%[ Get sudo configuration ]%></a>
+                                       </div>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third">
+                                               <com:TLabel
+                                                       ForControl="ChangerCommandTest"
+                                                       Text="<%[ Changer command test: ]%>"
+                                               /></div>
+                                       <div class="w3-col w3-half">
+                                               <table>
+                                                       <tr>
+                                                               <td>
+                                                                       <com:TActiveLinkButton
+                                                                               ID="ChangerCommandTest"
+                                                                               CssClass="w3-button w3-green"
+                                                                               CausesValidation="false"
+                                                                               OnCallback="testChangerCommand"
+                                                                       >
+                                                                               <prop:ClientSide.OnLoading>
+                                                                                       $('#changer_command_test_result_ok').hide();
+                                                                                       $('#changer_command_test_result_err').hide();
+                                                                                       $('#<%=$this->ChangerCommandTestResultErr->ClientID%>').hide();
+                                                                                       $('#changer_command_test_loader').show();
+                                                                               </prop:ClientSide.OnLoading>
+                                                                               <prop:ClientSide.OnComplete>
+                                                                                       $('#changer_command_test_loader').hide();
+                                                                               </prop:ClientSide.OnComplete>
+                                                                               <i class="fas fa-play"></i> &nbsp;<%[ test ]%>
+                                                                       </com:TActiveLinkButton>
+                                                               </td>
+                                                               <td style="padding-left: 10px">
+                                                                       <span id="changer_command_test_loader" style="display: none">
+                                                                               <i class="fas fa-sync fa-spin" title="<%[ Loading... ]%>"></i>
+                                                                       </span>
+                                                                       <span id="changer_command_test_result_ok" class="w3-text-green" style="display: none">
+                                                                               <i class="fas fa-check"></i> &nbsp;<%[ OK ]%>
+                                                                       </span>
+                                                                       <span id="changer_command_test_result_err" class="w3-text-red" style="display: none">
+                                                                               <i class="fas fa-exclamation-circle"></i> &nbsp;
+                                                                       </span>
+                                                                       <com:TActiveLabel ID="ChangerCommandTestResultErr" CssClass="w3-text-red" Display="None"><%[ Changer command error ]%></com:TActiveLabel>
+                                                               </td>
+                                                       </tr>
+                                               </table>
+                                       </div>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="ChangerCommand" Text="<%[ Devices: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveListBox
+                                                       ID="ChangerDevices"
+                                                       SelectionMode="Multiple"
+                                                       CssClass="w3-input w3-border"
+                                                       AutoPostBack="false"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="AutchangerGroup"
+                                                       ControlToValidate="ChangerDevices"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                               <p class="w3-text-black" style="margin-top: 8px;"><%[ Use CTRL + left-click to multiple item selection ]%></p>
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <footer class="w3-container w3-center">
+                                       <button type="button" class="w3-button w3-red" onclick="document.getElementById('autochanger_window').style.display = 'none';"><i class="fas fa-times"></i> &nbsp;<%[ Cancel ]%></button>
+                                       <com:TActiveLinkButton
+                                               ID="AutochangerSave"
+                                               ValidationGroup="AutchangerGroup"
+                                               CausesValidation="true"
+                                               OnCallback="saveAutochanger"
+                                               CssClass="w3-button w3-section w3-teal w3-padding"
+                                       >
+                                               <i class="fa fa-save"></i> &nbsp;<%[ Save ]%>
+                                       </com:TActiveLinkButton>
+                               </footer>
+                               <com:TActiveHiddenField ID="AutochangerWindowType" />
+                       </div>
+               </div>
+       </div>
+<com:TCallback ID="AutochangerList" OnCallback="setAutochangerList" />
+<com:TCallback ID="LoadAutochanger" OnCallback="loadAutochanger" />
+<com:TCallback ID="AddAutochanger" OnCallback="addAutochanger" />
+<com:TCallback ID="DeleteAutochanger" OnCallback="deleteAutochanger" />
+<script>
+var oAPIAutochangers = {
+       ids: {
+               autochanger_window: 'autochanger_window',
+               autochanger_name: '<%=$this->AutochangerName->ClientID%>',
+               autochanger_device: '<%=$this->ChangerDevice->ClientID%>',
+               autochanger_command: '<%=$this->ChangerCommand->ClientID%>',
+               autochanger_devices: '<%=$this->ChangerDevices->ClientID%>',
+               title_add: 'autochanger_window_title_add',
+               title_edit: 'autochanger_window_title_edit',
+               window_type: '<%=$this->AutochangerWindowType->ClientID%>',
+               autochanger_exists: 'autochanger_exists',
+               autochanger_config: 'autochanger_config'
+       },
+       achs_config: {},
+       init: function() {
+               this.set_events();
+               this.load_autochanger_list();
+       },
+       set_events: function() {
+               var send_form = function(e) {
+                       var kc = e.which || e.keyCode;
+                       if (kc == 13) {
+                               $('#<%=$this->AutochangerSave->ClientID%>').click();
+                       }
+               };
+               [
+                       this.ids.autochanger_name,
+                       this.ids.autochanger_device,
+                       this.ids.autochanger_command,
+                       this.ids.autochanger_devices
+               ].forEach(function(id) {
+                       document.getElementById(id).addEventListener('keypress', send_form);
+               });
+               var ach_conf = document.getElementById(this.ids.autochanger_config);
+               ach_conf.addEventListener('change', function(e) {
+                       var name = document.getElementById(this.ids.autochanger_name);
+                       var device = document.getElementById(this.ids.autochanger_device);
+                       var command = document.getElementById(this.ids.autochanger_command);
+                       if (ach_conf.value) {
+                               name.value = this.achs_config[ach_conf.value].Name;
+                               device.value = this.achs_config[ach_conf.value].ChangerDevice;
+                               command.value = this.achs_config[ach_conf.value].ChangerCommand;
+                       } else {
+                               name.value = device.value = command.value = '';
+                       }
+               }.bind(this));
+       },
+       show_autochanger_window: function(show, name) {
+               oAPIAutochangers.hide_errors();
+               oAPIAutochangers.clear_fields();
+               var win = document.getElementById(oAPIAutochangers.ids.autochanger_window);
+               if (show) {
+                       win.style.display = 'block';
+               } else {
+                       win.style.display = 'none';
+               }
+               var title_add = document.getElementById(oAPIAutochangers.ids.title_add);
+               var title_edit = document.getElementById(oAPIAutochangers.ids.title_edit);
+               var window_type = document.getElementById(oAPIAutochangers.ids.window_type);
+               var autochanger_name = document.getElementById(oAPIAutochangers.ids.autochanger_name);
+               if (name) {
+                       // edit autochanger
+                       this.load_autochanger_window(name);
+                       title_add.style.display = 'none';
+                       title_edit.style.display = '';
+                       window_type.value = '<%=APIDevices::WINDOW_TYPE_EDIT%>';
+                       autochanger_name.setAttribute('readonly', true);
+               } else {
+                       // add new autochanger
+                       title_edit.style.display = 'none';
+                       title_add.style.display = '';
+                       window_type.value = '<%=APIDevices::WINDOW_TYPE_ADD%>';
+                       autochanger_name.removeAttribute('readonly');
+                       autochanger_name.focus();
+               }
+       },
+       load_autochanger_window: function(name) {
+               var cb = <%=$this->LoadAutochanger->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(name);
+               cb.dispatch();
+       },
+       load_autochanger_list: function() {
+               var cb = <%=$this->AutochangerList->ActiveControl->Javascript%>;
+               cb.dispatch();
+       },
+       load_autochanger_list_cb: function(autochangers) {
+               oAutochangerList.data = autochangers;
+               oAutochangerList.init();
+       },
+       add_autochanger: function() {
+               var cb = <%=$this->AddAutochanger->ActiveControl->Javascript%>;
+               cb.dispatch();
+               this.show_autochanger_window(true);
+       },
+       edit_autochanger: function(name) {
+               this.show_autochanger_window(true, name);
+       },
+       delete_autochanger: function(name) {
+               if (!confirm('<%[ Are you sure? ]%>')) {
+                       return false;
+               }
+               var cb = <%=$this->DeleteAutochanger->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(name);
+               cb.dispatch();
+       },
+       set_disabled_drives: function(indices) {
+               oAPIAutochangers.clear_disabled_drives();
+               var devices = document.getElementById(oAPIAutochangers.ids.autochanger_devices);
+               for (var i = 0; i < indices.length; i++) {
+                       devices.options[indices[i]].setAttribute('disabled', 'disabled');
+               }
+       },
+       clear_disabled_drives: function() {
+               var devices = document.getElementById(oAPIAutochangers.ids.autochanger_devices);
+               for (var i = 0; i < devices.options.length; i++) {
+                       if (devices.options[i].hasAttribute('disabled')) {
+                               devices.options[i].removeAttribute('disabled');
+                       }
+               }
+       },
+       hide_errors: function() {
+               document.getElementById(this.ids.autochanger_exists).style.display = 'none';
+               // changer command test result
+               $('#changer_command_test_result_ok').hide();
+               $('#changer_command_test_result_err').hide();
+               $('#<%=$this->ChangerCommandTestResultErr->ClientID%>').hide();
+       },
+       clear_fields: function() {
+               [
+                       this.ids.autochanger_name,
+                       this.ids.autochanger_device,
+                       this.ids.autochanger_command,
+                       this.ids.autochanger_devices
+               ].forEach(function(id) {
+                       document.getElementById(id).value = '';
+               });
+       },
+       set_config_autochangers: function(achs) {
+               if (achs.exitcode !== 0) {
+                       return false;
+               }
+
+               var combo = document.getElementById(oAPIAutochangers.ids.autochanger_config);
+               var opt = document.createElement('OPTION');
+               opt.value = '';
+               var label = document.createTextNode('');
+               opt.appendChild(label);
+               combo.appendChild(opt);
+
+               for (var i = 0; i < achs.output.length; i++) {
+                       opt = document.createElement('OPTION');
+                       opt.value = achs.output[i].Autochanger.Name;
+                       label = document.createTextNode(achs.output[i].Autochanger.Name);
+                       opt.appendChild(label);
+                       combo.appendChild(opt);
+                       oAPIAutochangers.achs_config[achs.output[i].Autochanger.Name] = achs.output[i].Autochanger;
+               }
+       }
+};
+$(function() {
+       oAPIAutochangers.init();
+});
+</script>
+       <div class="w3-container tab_item" id="devices_device" style="display: none">
+               <a href="javascript:void(0)" class="w3-button w3-green w3-margin-bottom" onclick="oAPIDevices.show_device_window(true);">
+                       <i class="fas fa-plus"></i> &nbsp;<%[ Add device ]%>
+               </a>
+               <table id="device_list" class="w3-table w3-striped w3-hoverable w3-white w3-margin-bottom" style="width: 100%">
+                       <thead>
+                               <tr>
+                                       <th></th>
+                                       <th><%[ Name ]%></th>
+                                       <th><%[ Device ]%></th>
+                                       <th><%[ Drive index ]%></th>
+                                       <th><%[ Autochanger ]%></th>
+                                       <th><%[ Actions ]%></th>
+                               </tr>
+                       </thead>
+                       <tbody id="device_list_body"></tbody>
+                       <tfoot>
+                               <tr>
+                                       <th></th>
+                                       <th><%[ Name ]%></th>
+                                       <th><%[ Device ]%></th>
+                                       <th><%[ Drive index ]%></th>
+                                       <th><%[ Autochanger ]%></th>
+                                       <th><%[ Actions ]%></th>
+                               </tr>
+                       </tfoot>
+               </table>
+       </div>
+<script>
+var oDeviceList = {
+       ids: {
+               device_list: 'device_list',
+               device_list_body: 'device_list_body'
+       },
+       table: null,
+       data: [],
+       init: function() {
+               if (!this.table) {
+                       this.set_table();
+               } else {
+                       var page = this.table.page();
+                       this.table.clear().rows.add(this.data).draw();
+                       this.table.page(page).draw(false);
+               }
+       },
+       set_table: function() {
+               this.table = $('#' + this.ids.device_list).DataTable({
+                       data: this.data,
+                       deferRender: true,
+                       dom: 'lBfrtip',
+                       stateSave: true,
+                       buttons: [
+                               'copy', 'csv', 'colvis'
+                       ],
+                       columns: [
+                               {
+                                       className: 'details-control',
+                                       orderable: false,
+                                       data: null,
+                                       defaultContent: '<button type="button" class="w3-button w3-blue"><i class="fa fa-angle-down"></i></button>'
+                               },
+                               {data: 'name'},
+                               {data: 'device'},
+                               {data: 'index'},
+                               {data: 'autochanger'},
+                               {
+                                       data: 'name',
+                                       render: function(data, type, row) {
+                                               var span = document.createElement('SPAN');
+                                               span.className = 'w3-right';
+
+                                               var chpwd_btn = document.createElement('BUTTON');
+                                               chpwd_btn.className = 'w3-button w3-green';
+                                               chpwd_btn.type = 'button';
+                                               var i = document.createElement('I');
+                                               i.className = 'fas fa-edit';
+                                               var label = document.createTextNode(' <%[ Edit ]%>');
+                                               chpwd_btn.appendChild(i);
+                                               chpwd_btn.innerHTML += '&nbsp';
+                                               chpwd_btn.appendChild(label);
+                                               chpwd_btn.setAttribute('onclick', 'oAPIDevices.edit_device("' + data + '")');
+
+                                               var del_btn = document.createElement('BUTTON');
+                                               del_btn.className = 'w3-button w3-red w3-margin-left';
+                                               del_btn.type = 'button';
+                                               var i = document.createElement('I');
+                                               i.className = 'fas fa-trash-alt';
+                                               var label = document.createTextNode(' <%[ Delete ]%>');
+                                               del_btn.appendChild(i);
+                                               del_btn.innerHTML += '&nbsp';
+                                               del_btn.appendChild(label);
+                                               del_btn.setAttribute('onclick', 'oAPIDevices.delete_device("' + data + '", "' + row.autochanger + '")');
+
+                                               span.appendChild(chpwd_btn);
+                                               span.appendChild(del_btn);
+
+                                               return span.outerHTML;
+                                       }
+                               }
+                       ],
+                       responsive: {
+                               details: {
+                                       type: 'column'
+                               }
+                       },
+                       columnDefs: [{
+                               className: 'control',
+                               orderable: false,
+                               targets: 0
+                       },
+                       {
+                               className: "dt-center",
+                               targets: [ 3, 5 ]
+                       }],
+                       order: [1, 'asc'],
+                       initComplete: function () {
+                               oDeviceList.set_filters(this.api());
+                       }
+               });
+       },
+       set_filters: function(api) {
+               api.columns([2, 3, 4]).every(function () {
+                       var column = this;
+                       var select = $('<select><option value=""></option></select>')
+                       .appendTo($(column.footer()).empty())
+                       .on('change', function () {
+                               var val = dtEscapeRegex(
+                                       $(this).val()
+                               );
+                               column
+                               .search(val ? '^' + val + '$' : '', true, false)
+                               .draw();
+                       });
+                       column.data().unique().sort().each(function (d, j) {
+                               if (column.search() == '^' + dtEscapeRegex(d) + '$') {
+                                       select.append('<option value="' + d + '" title="' + d + '" selected>' + d + '</option>');
+                               } else if (d) {
+                                       select.append('<option value="' + d + '" title="' + d + '">' + d + '</option>');
+                               }
+                       });
+               });
+       },
+};
+</script>
+       <div id="device_window" class="w3-modal">
+               <div class="w3-modal-content w3-animate-top w3-card-4">
+                       <header class="w3-container w3-teal">
+                               <span onclick="document.getElementById('device_window').style.display = 'none';" class="w3-button w3-display-topright">&times;</span>
+                               <h2 id="device_window_title_add" style="display: none"><%[ Add device ]%></h2>
+                               <h2 id="device_window_title_edit" style="display: none"><%[ Edit device ]%></h2>
+                       </header>
+                       <div class="w3-container w3-margin-left w3-margin-right w3-text-teal">
+                               <span id="device_exists" class="error" style="display: none"><ul><li><%[ Device with the given name already exists. ]%></li></ul></span>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><label for="device_config"><%[ Copy from Bacula SD config: ]%></label></div>
+                                       <div class="w3-half">
+                                               <select id="device_config" class="w3-select w3-border"></select>
+                                       </div>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="DeviceName" Text="<%[ Name: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveTextBox
+                                                       ID="DeviceName"
+                                                       AutoPostBack="false"
+                                                       MaxLength="100"
+                                                       CssClass="w3-input w3-border"
+                                                       Attributes.placeholder="My Device 123"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="DeviceGroup"
+                                                       ControlToValidate="DeviceName"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                               <com:TRegularExpressionValidator
+                                                       ValidationGroup="DeviceGroup"
+                                                       RegularExpression="<%=DeviceConfig::DEVICE_PATH_PATTERN%>"
+                                                       ControlToValidate="DeviceName"
+                                                       ErrorMessage="<%[ Invalid value. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="ChangerDevice" Text="<%[ Device path: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveTextBox
+                                                       ID="DeviceDevice"
+                                                       AutoPostBack="false"
+                                                       MaxLength="100"
+                                                       CssClass="w3-input w3-border"
+                                                       Attributes.placeholder="/dev/nst1"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="DeviceGroup"
+                                                       ControlToValidate="DeviceDevice"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <div class="w3-row w3-section">
+                                       <div class="w3-col w3-third"><com:TLabel ForControl="ChangerCommand" Text="<%[ Drive index: ]%>" /></div>
+                                       <div class="w3-half">
+                                               <com:TActiveTextBox
+                                                       ID="DeviceIndex"
+                                                       AutoPostBack="false"
+                                                       MaxLength="4"
+                                                       Style.Width="100px"
+                                                       CssClass="w3-input w3-border"
+                                                       Attributes.placeholder="0"
+                                               />
+                                               <com:TRequiredFieldValidator
+                                                       ValidationGroup="DeviceGroup"
+                                                       ControlToValidate="DeviceIndex"
+                                                       ErrorMessage="<%[ Field cannot be empty. ]%>"
+                                                       ControlCssClass="field_invalid"
+                                                       Display="Dynamic"
+                                               />
+                                       </div> &nbsp;<i id="device_required" class="fa fa-asterisk w3-text-red opt_req" style="display none"></i>
+                               </div>
+                               <footer class="w3-container w3-center">
+                                       <button type="button" class="w3-button w3-red" onclick="document.getElementById('device_window').style.display = 'none';"><i class="fas fa-times"></i> &nbsp;<%[ Cancel ]%></button>
+                                       <com:TActiveLinkButton
+                                               ID="DeviceSave"
+                                               ValidationGroup="DeviceGroup"
+                                               CausesValidation="true"
+                                               OnCallback="saveDevice"
+                                               CssClass="w3-button w3-section w3-teal w3-padding"
+                                       >
+                                               <i class="fa fa-save"></i> &nbsp;<%[ Save ]%>
+                                       </com:TActiveLinkButton>
+                               </footer>
+                               <com:TActiveHiddenField ID="DeviceWindowType" />
+                       </div>
+               </div>
+       </div>
+<com:TCallback ID="DeviceList" OnCallback="setDeviceList" />
+<com:TCallback ID="LoadDevice" OnCallback="loadDevice" />
+<com:TCallback ID="DeleteDevice" OnCallback="deleteDevice" />
+<script>
+var oAPIDevices = {
+       ids: {
+               device_window: 'device_window',
+               device_name: '<%=$this->DeviceName->ClientID%>',
+               device_device: '<%=$this->DeviceDevice->ClientID%>',
+               device_index: '<%=$this->DeviceIndex->ClientID%>',
+               title_add: 'device_window_title_add',
+               title_edit: 'device_window_title_edit',
+               window_type: '<%=$this->DeviceWindowType->ClientID%>',
+               device_exists: 'device_exists',
+               device_config: 'device_config'
+       },
+       devs_config: {},
+       init: function() {
+               this.set_events();
+               this.load_device_list();
+       },
+       set_events: function() {
+               var send_form = function(e) {
+                       var kc = e.which || e.keyCode;
+                       if (kc == 13) {
+                               $('#<%=$this->DeviceSave->ClientID%>').click();
+                       }
+               };
+               [
+                       this.ids.device_name,
+                       this.ids.device_device,
+                       this.ids.device_index
+               ].forEach(function(id) {
+                       document.getElementById(id).addEventListener('keypress', send_form);
+               });
+               var dev_conf = document.getElementById(this.ids.device_config);
+               dev_conf.addEventListener('change', function(e) {
+                       var name = document.getElementById(this.ids.device_name);
+                       var device = document.getElementById(this.ids.device_device);
+                       var index = document.getElementById(this.ids.device_index);
+                       if (dev_conf.value) {
+                               name.value = this.devs_config[dev_conf.value].Name;
+                               device.value = this.devs_config[dev_conf.value].ArchiveDevice;
+                               index.value = this.devs_config[dev_conf.value].DriveIndex;
+                       } else {
+                               name.value = device.value = index.value = '';
+                       }
+               }.bind(this));
+       },
+       show_device_window: function(show, name) {
+               oAPIDevices.hide_errors();
+               oAPIDevices.clear_fields();
+               var win = document.getElementById(oAPIDevices.ids.device_window);
+               if (show) {
+                       win.style.display = 'block';
+               } else {
+                       win.style.display = 'none';
+               }
+               var title_add = document.getElementById(oAPIDevices.ids.title_add);
+               var title_edit = document.getElementById(oAPIDevices.ids.title_edit)
+               var window_type = document.getElementById(oAPIDevices.ids.window_type);
+               var device_name = document.getElementById(oAPIDevices.ids.device_name);
+               if (name) {
+                       // edit device
+                       oAPIDevices.load_device_window(name);
+                       window_type.value = '<%=APIDevices::WINDOW_TYPE_EDIT%>';
+                       title_add.style.display = 'none';
+                       title_edit.style.display = '';
+                       device_name.setAttribute('readonly', true);
+               } else {
+                       // add new device
+                       window_type.value = '<%=APIDevices::WINDOW_TYPE_ADD%>';
+                       title_edit.style.display = 'none';
+                       title_add.style.display = '';
+                       device_name.removeAttribute('readonly');
+                       device_name.focus();
+               }
+       },
+       load_device_window: function(name) {
+               var cb = <%=$this->LoadDevice->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(name);
+               cb.dispatch();
+       },
+       load_device_list: function() {
+               var cb = <%=$this->DeviceList->ActiveControl->Javascript%>;
+               cb.dispatch();
+       },
+       load_device_list_cb: function(devices) {
+               oDeviceList.data = devices;
+               oDeviceList.init();
+       },
+       edit_device: function(name) {
+               this.show_device_window(true, name);
+       },
+       delete_device: function(name, autochanger) {
+               if (autochanger) {
+                       var emsg = "<%[ Unable to delete device. Please unassign it from autochanger '%s' first. ]%>";
+                       emsg = emsg.replace('%s', autochanger);
+                       alert(emsg);
+                       return false;
+               }
+               if (!confirm('<%[ Are you sure? ]%>')) {
+                       return false;
+               }
+               var cb = <%=$this->DeleteDevice->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(name);
+               cb.dispatch();
+       },
+       hide_errors: function() {
+               document.getElementById(this.ids.device_exists).style.display = 'none';
+       },
+       clear_fields: function() {
+               [
+                       this.ids.device_name,
+                       this.ids.device_device,
+                       this.ids.device_index
+               ].forEach(function(id) {
+                       document.getElementById(id).value = '';
+               });
+       },
+       set_config_devices: function(devs) {
+               if (devs.exitcode !== 0) {
+                       return false;
+               }
+
+               var combo = document.getElementById(oAPIDevices.ids.device_config);
+               var opt = document.createElement('OPTION');
+               opt.value = '';
+               var label = document.createTextNode('');
+               opt.appendChild(label);
+               combo.appendChild(opt);
+
+               for (var i = 0; i < devs.output.length; i++) {
+                       opt = document.createElement('OPTION');
+                       opt.value = devs.output[i].Device.Name;
+                       label = document.createTextNode(devs.output[i].Device.Name);
+                       opt.appendChild(label);
+                       combo.appendChild(opt);
+                       oAPIDevices.devs_config[devs.output[i].Device.Name] = devs.output[i].Device;
+               }
+       }
+};
+$(function() {
+       oAPIDevices.init();
+});
+</script>
+</com:TContent>
diff --git a/gui/baculum/protected/API/Pages/Panel/APIDevices.php b/gui/baculum/protected/API/Pages/Panel/APIDevices.php
new file mode 100644 (file)
index 0000000..8958502
--- /dev/null
@@ -0,0 +1,321 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('Application.API.Class.DeviceConfig');
+Prado::using('Application.API.Class.BAPIException');
+Prado::using('Application.API.Class.BaculumAPIPage');
+
+/**
+ * API devices page.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category Panel
+ * @package Baculum API
+ */
+class APIDevices extends BaculumAPIPage {
+
+       const WINDOW_TYPE_ADD = 'add';
+       const WINDOW_TYPE_EDIT = 'edit';
+
+       private $config;
+
+       public function onInit($param) {
+               parent::onInit($param);
+               $this->config = $this->getModule('device_config')->getConfig();
+       }
+
+       public function setAutochangerList($sender, $param) {
+               $devices = [];
+               foreach ($this->config as $name => $device) {
+                       if ($device['type'] !== DeviceConfig::DEV_TYPE_AUTOCHANGER) {
+                               continue;
+                       }
+                       $device['name'] = $name;
+                       $devices[] = $device;
+               }
+
+               $this->getCallbackClient()->callClientFunction(
+                       'oAPIAutochangers.load_autochanger_list_cb',
+                       [$devices]
+               );
+
+               if (is_object($sender)) {
+                       $this->setConfigAutochangers();
+               }
+       }
+
+       public function addAutochanger($sender, $param) {
+               $ach_drives = $this->getAutochangerDrives();
+               $disabled_indices = [];
+               for ($i = 0; $i < $this->ChangerDevices->getItemCount(); $i++) {
+                       $item = $this->ChangerDevices->Items[$i];
+                       if (key_exists($item->Value, $ach_drives)) {
+                               $disabled_indices[] = $i;
+                       }
+               }
+
+               $this->getCallbackClient()->callClientFunction(
+                       'oAPIAutochangers.set_disabled_drives',
+                       [$disabled_indices]
+               );
+       }
+
+       public function loadAutochanger($sender, $param) {
+               $ach_name = $param->getCallbackParameter();
+               $ach = [];
+               foreach ($this->config as $name => $device) {
+                       if ($device['type'] !== DeviceConfig::DEV_TYPE_AUTOCHANGER) {
+                               continue;
+                       }
+                       if ($name == $ach_name) {
+                               $ach = $device;
+                               break;
+                       }
+               }
+               if (count($ach) > 0) {
+                       $this->AutochangerName->Text = $name;
+                       $this->ChangerDevice->Text = $ach['device'];
+                       $this->ChangerCommand->Text = $ach['command'];
+                       $this->ChangerCommandUseSudo->Checked = ($ach['use_sudo'] == 1);
+                       $drives = explode(',', $ach['drives']);
+                       $ach_drives = $this->getAutochangerDrives();
+                       $disabled_indices = [];
+                       $selected_indices = [];
+                       for ($i = 0; $i < $this->ChangerDevices->getItemCount(); $i++) {
+                               $item = $this->ChangerDevices->Items[$i];
+                               if (key_exists($item->Value, $ach_drives) && $ach_drives[$item->Value] !== $name) {
+                                       $disabled_indices[] = $i;
+                                       continue;
+                               }
+                               if (in_array($item->Value, $drives)) {
+                                       $selected_indices[] = $i;
+                               }
+                       }
+                       $this->ChangerDevices->setSelectedIndices($selected_indices);
+                       $this->getCallbackClient()->callClientFunction(
+                               'oAPIAutochangers.set_disabled_drives',
+                               [$disabled_indices]
+                       );
+               }
+       }
+
+       public function saveAutochanger($sender, $param) {
+               if ($this->AutochangerWindowType->Value == self::WINDOW_TYPE_ADD && key_exists($this->AutochangerName->Text, $this->config)) {
+                       $this->getCallbackClient()->show('autochanger_exists');
+                       return;
+               }
+               $drives = [];
+               $selected_indices = $this->ChangerDevices->getSelectedIndices();
+               foreach ($selected_indices as $indice) {
+                       for ($i = 0; $i < $this->ChangerDevices->getItemCount(); $i++) {
+                               if ($i === $indice) {
+                                       $drives[] = $this->ChangerDevices->Items[$i]->Value;
+                               }
+                       }
+               }
+               $autochanger = [
+                       'type' => DeviceConfig::DEV_TYPE_AUTOCHANGER,
+                       'device' => $this->ChangerDevice->Text,
+                       'command' => $this->ChangerCommand->Text,
+                       'use_sudo' => $this->ChangerCommandUseSudo->Checked ? '1' : '0',
+                       'drives'=> implode(',', $drives)
+               ];
+               $this->config[$this->AutochangerName->Text] = $autochanger;
+               $result = $this->getModule('device_config')->setConfig($this->config);
+               if ($result) {
+                       $this->getCallbackClient()->callClientFunction('oAPIAutochangers.show_autochanger_window', [false]);
+                       $this->setAutochangerList(null, null);
+                       $this->setDeviceList(null, null);
+               }
+       }
+
+       public function deleteAutochanger($sender, $param) {
+               $ach = $param->getCallbackParameter();
+               if (!key_exists($ach, $this->config)) {
+                       return;
+               }
+               unset($this->config[$ach]);
+               $result = $this->getModule('device_config')->setConfig($this->config);
+               if ($result) {
+                       $this->setAutochangerList(null, null);
+                       $this->setDeviceList(null, null);
+               }
+       }
+
+       public function testChangerCommand($sender, $param) {
+               $emsg = '';
+               $use_sudo = $this->ChangerCommandUseSudo->Checked;
+               $changer_command = $this->ChangerCommand->Text;
+               $changer_device = $this->ChangerDevice->Text;
+               $command = 'listall';
+               // slot, archive device and index are not used in listall cmd
+               $slot = 0;
+               $archive_device = '/dev/null';
+               $drive_index = 0;
+               $is_validate = false;
+               if (!empty($changer_command) && !empty($changer_device)) {
+                       $result = $this->getModule('changer_command')->testChangerCommand(
+                               $use_sudo,
+                               $changer_command,
+                               $changer_device,
+                               $command,
+                               $slot,
+                               $archive_device,
+                               $drive_index
+                       );
+                       $is_validate = ($result->error === 0);
+                       if (!$is_validate) {
+                               $this->ChangerCommandTestResultErr->Text = implode(PHP_EOL, $result->output);
+                       }
+               }
+               if ($is_validate === true) {
+                       $this->getCallbackClient()->show('changer_command_test_result_ok');
+                       $this->getCallbackClient()->hide('changer_command_test_result_err');
+                       $this->getCallbackClient()->hide($this->ChangerCommandTestResultErr);
+               } else {
+                       $this->getCallbackClient()->hide('changer_command_test_result_ok');
+                       $this->getCallbackClient()->show('changer_command_test_result_err');
+                       $this->getCallbackClient()->show($this->ChangerCommandTestResultErr);
+               }
+       }
+
+       public function setDeviceList($sender, $param) {
+               $devices = [];
+               $ach_devices = $this->getAutochangerDrives();
+               $dev_names = [];
+               foreach ($this->config as $name => $device) {
+                       if ($device['type'] !== DeviceConfig::DEV_TYPE_DEVICE) {
+                               continue;
+                       }
+                       $device['name'] = $name;
+                       $device['autochanger'] = (key_exists($name, $ach_devices)) ? $ach_devices[$name] : '';
+                       $dev_names[] = $name;
+                       $devices[] = $device;
+               }
+
+               // Set changer device select list
+               $this->ChangerDevices->DataSource = array_combine($dev_names, $dev_names);
+               $this->ChangerDevices->dataBind();
+
+               $this->getCallbackClient()->callClientFunction(
+                       'oAPIDevices.load_device_list_cb',
+                       [$devices]
+               );
+
+               if (is_object($sender)) {
+                       $this->setConfigDevices();
+               }
+       }
+
+       private function getAutochangerDrives() {
+               $ach_devices = [];
+               foreach ($this->config as $name => $device) {
+                       if ($device['type'] !== DeviceConfig::DEV_TYPE_AUTOCHANGER) {
+                               continue;
+                       }
+                       $drives = explode(',', $device['drives']);
+                       for ($i = 0; $i < count($drives); $i++) {
+                               $ach_devices[$drives[$i]] = $name;
+                       }
+               }
+               return $ach_devices;
+       }
+
+       public function loadDevice($sender, $param) {
+               $dev_name = $param->getCallbackParameter();
+               $dev = [];
+               foreach ($this->config as $name => $device) {
+                       if ($device['type'] !== DeviceConfig::DEV_TYPE_DEVICE) {
+                               continue;
+                       }
+                       if ($name == $dev_name) {
+                               $dev = $device;
+                               break;
+                       }
+               }
+               if (count($dev) > 0) {
+                       $this->DeviceName->Text = $name;
+                       $this->DeviceDevice->Text = $dev['device'];
+                       $this->DeviceIndex->Text = $dev['index'];
+               }
+       }
+
+       public function saveDevice($sender, $param) {
+               if ($this->DeviceWindowType->Value == self::WINDOW_TYPE_ADD && key_exists($this->DeviceName->Text, $this->config)) {
+                       $this->getCallbackClient()->show('device_exists');
+                       return;
+               }
+               $device = [
+                       'type' => DeviceConfig::DEV_TYPE_DEVICE,
+                       'device' => $this->DeviceDevice->Text,
+                       'index' => intval($this->DeviceIndex->Text)
+               ];
+               $this->config[$this->DeviceName->Text] = $device;
+               $result = $this->getModule('device_config')->setConfig($this->config);
+               if ($result) {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oAPIDevices.show_device_window',
+                               [false]
+                       );
+                       $this->setDeviceList(null, null);
+               }
+       }
+
+       public function deleteDevice($sender, $param) {
+               $device = $param->getCallbackParameter();
+               if (!key_exists($device, $this->config)) {
+                       return;
+               }
+               unset($this->config[$device]);
+               $result = $this->getModule('device_config')->setConfig($this->config);
+               if ($result) {
+                       $this->setDeviceList(null, null);
+               }
+       }
+
+       private function setConfigAutochangers() {
+               $achs = [];
+               try {
+                       $achs = $this->getModule('bacula_setting')->getConfig('sd', 'Autochanger');
+               } catch (BConfigException $e) {
+                       // do nothing
+               }
+               $this->getCallbackClient()->callClientFunction(
+                       'oAPIAutochangers.set_config_autochangers',
+                       [$achs]
+               );
+       }
+
+       private function setConfigDevices() {
+               $devs = [];
+               try {
+                       $devs = $this->getModule('bacula_setting')->getConfig('sd', 'Device');
+               } catch (BConfigException $e) {
+                       // do nothing
+               }
+               $this->getCallbackClient()->callClientFunction(
+                       'oAPIDevices.set_config_devices',
+                       [$devs]
+               );
+       }
+}
+?>
index 8ef6fc3d4f0e86237f5ff37c75074b0e618a440e..0c49cd0fa09bab3f95910a80e23d3ce63188bcb8 100644 (file)
                                                <com:TCheckBox
                                                        ID="UseSudo"
                                                        CssClass="w3-check"
-                                               /> &nbsp;<a href="javascript:void(0)" onclick="get_sudo_config('bconsole');"><%[ Get sudo configuration ]%></a>
+                                               /> &nbsp;<a href="javascript:void(0)" onclick="oSudoConfig.get_config('bconsole');"><%[ Get sudo configuration ]%></a>
 
                                        </div>
                                </div>
                                                        <com:TCheckBox
                                                                ID="BJSONUseSudo"
                                                                CssClass="w3-check"
-                                                       /> &nbsp;<a href="javascript:void(0)" onclick="get_sudo_config('config');"><%[ Get sudo configuration ]%></a>
+                                                       /> &nbsp;<a href="javascript:void(0)" onclick="oSudoConfig.get_config('config');"><%[ Get sudo configuration ]%></a>
                                                </div>
                                        </div>
                                </fieldset>
                        </fieldset>
                </com:TWizardStep>
        </com:TWizard>
-<com:TJuiDialog
-       ID="SudoConfigPopup"
-       Options.title="<%[ Sudo configuration ]%>"
-       Options.autoOpen="false",
-       Options.minWidth="820"
-       Options.minHeight="200"
->
-       <p><%[ Please copy appropriate sudo configuration and put it to a new sudoers.d file for example /etc/sudoers.d/baculum-api ]%></p>
-       <p><strong><%[ Note ]%></strong> <%[ Please use visudo to add this configuration, otherwise please do remember to add empty line at the end of file. ]%>
-       <p><%[ Example sudo configuration for Apache web server user (RHEL, CentOS and others): ]%></p>
-       <pre id="sudo_config_apache"></pre>
-       <p><%[ Example sudo configuration for Lighttpd web server user (RHEL, CentOS and others): ]%></p>
-       <pre id="sudo_config_lighttpd"></pre>
-       <p><%[ Example sudo configuration for Apache and Lighttpd web servers user (Debian, Ubuntu and others): ]%></p>
-       <pre id="sudo_config_www_data"></pre>
-</com:TJuiDialog>
+       <com:Application.API.Portlets.SudoConfig ID="SudoConfig" />
 <script type="text/javascript">
 var bjsontools_fields = {
        General: {
@@ -1371,33 +1356,17 @@ var actions_hide_test_results = function(action) {
        result.textContent = '';
 };
 
-function get_sudo_config(type) {
-       var bin_fields = {
-               bconsole: [
-                       '<%=$this->BconsolePath->ClientID%>'
-               ],
-               config: [
-                       '<%=$this->BDirJSONPath->ClientID%>',
-                       '<%=$this->BSdJSONPath->ClientID%>',
-                       '<%=$this->BFdJSONPath->ClientID%>',
-                       '<%=$this->BBconsJSONPath->ClientID%>',
-               ]
-       }
-       var val, pre;
-       var cfg = '';
-       var users = ['apache', 'lighttpd', 'www-data'];
-       var fields = bin_fields.hasOwnProperty(type) ? bin_fields[type] : [];
-       for (var i = 0; i < users.length; i++) {
-               var pre = document.getElementById('sudo_config_' + users[i].replace(/-/g, '_'));
-               pre.textContent = 'Defaults:' + users[i] + ' !requiretty' + "\n";
-               for (var j = 0; j < fields.length; j++) {
-                       val = document.getElementById(fields[j]).value.trim();
-                       if (val) {
-                               pre.textContent += users[i] + ' ALL = (root) NOPASSWD: ' + val + "\n";
-                       }
-               }
-       }
-       $('#<%=$this->SudoConfigPopup->ClientID%>').dialog('open');
-}
+var bin_fields = {
+       bconsole: [
+               '<%=$this->BconsolePath->ClientID%>'
+       ],
+       config: [
+               '<%=$this->BDirJSONPath->ClientID%>',
+               '<%=$this->BSdJSONPath->ClientID%>',
+               '<%=$this->BFdJSONPath->ClientID%>',
+               '<%=$this->BBconsJSONPath->ClientID%>',
+       ]
+};
+oSudoConfig.set_bin_fields(bin_fields);
 </script>
 </com:TContent>
index e4dc9cfdb20dba41fee36aa6a8487a74389fdd7d..5c9380b2e1fa6ecb4b5de3af09a9a1f8f7afe613 100644 (file)
                                        <com:TCheckBox
                                                ID="BconsoleUseSudo"
                                                CssClass="w3-check"
-                                       /> &nbsp;<a href="javascript:void(0)" onclick="get_sudo_config('bconsole');"><%[ Get sudo configuration ]%></a>
+                                       /> &nbsp;<a href="javascript:void(0)" onclick="oSudoConfig.get_config('bconsole');"><%[ Get sudo configuration ]%></a>
                                </div>
                        </div>
                        <div class="w3-row w3-section">
                                                <com:TCheckBox
                                                        ID="BJSONUseSudo"
                                                        CssClass="w3-check"
-                                               /> &nbsp;<a href="javascript:void(0)" onclick="get_sudo_config('config');"><%[ Get sudo configuration ]%></a>
+                                               /> &nbsp;<a href="javascript:void(0)" onclick="oSudoConfig.get_config('config');"><%[ Get sudo configuration ]%></a>
                                        </div>
                                </div>
                        </fieldset>
                                                <com:TCheckBox
                                                        ID="ActionsUseSudo"
                                                        CssClass="w3-check"
-                                               /> &nbsp;<a href="javascript:void(0)" onclick="get_sudo_config('actions');"><%[ Get sudo configuration ]%></a>
+                                               /> &nbsp;<a href="javascript:void(0)" onclick="oSudoConfig.get_config('actions');"><%[ Get sudo configuration ]%></a>
                                        </div>
                                </div>
                        </fieldset>
                        </div>
                </div>
        </div>
-<com:TJuiDialog
-       ID="SudoConfigPopup"
-       Options.title="<%[ Sudo configuration ]%>"
-       Options.autoOpen="false",
-       Options.minWidth="820"
-       Options.minHeight="200"
->
-       <p><%[ Please copy appropriate sudo configuration and put it to a new sudoers.d file for example /etc/sudoers.d/baculum-api ]%></p>
-       <p><strong><%[ Note ]%></strong> <%[ Please use visudo to add this configuration, otherwise please do remember to add empty line at the end of file. ]%>
-       <p><%[ Example sudo configuration for Apache web server user (RHEL, CentOS and others): ]%></p>
-       <pre id="sudo_config_apache"></pre>
-       <p><%[ Example sudo configuration for Lighttpd web server user (RHEL, CentOS and others): ]%></p>
-       <pre id="sudo_config_lighttpd"></pre>
-       <p><%[ Example sudo configuration for Apache and Lighttpd web servers user (Debian, Ubuntu and others): ]%></p>
-       <pre id="sudo_config_www_data"></pre>
-</com:TJuiDialog>
+       <com:Application.API.Portlets.SudoConfig ID="SudoConfig" />
 <script>
-function get_sudo_config(type) {
-       var bin_fields = {
-               bconsole: [
-                       '<%=$this->BconsolePath->ClientID%>'
-               ],
-               config: [
-                       '<%=$this->BDirJSONPath->ClientID%>',
-                       '<%=$this->BSdJSONPath->ClientID%>',
-                       '<%=$this->BFdJSONPath->ClientID%>',
-                       '<%=$this->BBconsJSONPath->ClientID%>',
-               ],
-               actions: [
-                       '<%=$this->DirStartAction->ClientID%>',
-                       '<%=$this->DirStopAction->ClientID%>',
-                       '<%=$this->DirRestartAction->ClientID%>',
-                       '<%=$this->SdStartAction->ClientID%>',
-                       '<%=$this->SdStopAction->ClientID%>',
-                       '<%=$this->SdRestartAction->ClientID%>',
-                       '<%=$this->FdStartAction->ClientID%>',
-                       '<%=$this->FdStopAction->ClientID%>',
-                       '<%=$this->FdRestartAction->ClientID%>'
-               ]
-       }
-       var val, pre;
-       var cfg = '';
-       var users = ['apache', 'lighttpd', 'www-data'];
-       var fields = bin_fields.hasOwnProperty(type) ? bin_fields[type] : [];
-       for (var i = 0; i < users.length; i++) {
-               var pre = document.getElementById('sudo_config_' + users[i].replace(/-/g, '_'));
-               pre.textContent = 'Defaults:' + users[i] + ' !requiretty' + "\n";
-               for (var j = 0; j < fields.length; j++) {
-                       val = document.getElementById(fields[j]).value.trim();
-                       if (val) {
-                               pre.textContent += users[i] + ' ALL = (root) NOPASSWD: ' + val + "\n";
-                       }
-               }
-       }
-       $('#<%=$this->SudoConfigPopup->ClientID%>').dialog('open');
-}
+var bin_fields = {
+       bconsole: [
+               '<%=$this->BconsolePath->ClientID%>'
+       ],
+       config: [
+               '<%=$this->BDirJSONPath->ClientID%>',
+               '<%=$this->BSdJSONPath->ClientID%>',
+               '<%=$this->BFdJSONPath->ClientID%>',
+               '<%=$this->BBconsJSONPath->ClientID%>',
+       ],
+       actions: [
+               '<%=$this->DirStartAction->ClientID%>',
+               '<%=$this->DirStopAction->ClientID%>',
+               '<%=$this->DirRestartAction->ClientID%>',
+               '<%=$this->SdStartAction->ClientID%>',
+               '<%=$this->SdStopAction->ClientID%>',
+               '<%=$this->SdRestartAction->ClientID%>',
+               '<%=$this->FdStartAction->ClientID%>',
+               '<%=$this->FdStopAction->ClientID%>',
+               '<%=$this->FdRestartAction->ClientID%>'
+       ]
+};
+oSudoConfig.set_bin_fields(bin_fields);
 
 var bjsontools_fields = {
        General: {
index 399a78e79ad02a211c601db451c2ae44c0745521..dfc9b44e7d0c570b93bc1df606e02b3d104adb5a 100644 (file)
@@ -10,6 +10,8 @@
                <module id="json_tools" class="Application.API.Class.JSONTools" />
                <!-- config modules -->
                <module id="api_config" class="Application.API.Class.APIConfig" />
+               <module id="device_config" class="Application.API.Class.DeviceConfig" />
+               <module id="bacula_setting" class="Application.API.Class.BaculaSetting" />
                <!-- internalization modules -->
                <module id="globalization" class="TGlobalization">
                        <translation type="gettext" source="Application.API.Lang" marker="@@" autosave="false" cache="false" DefaultCulture="en" />
@@ -23,5 +25,7 @@
                <module id="oauth2_config" class="Application.API.Class.OAuth2.OAuth2Config" />
                <!-- component actions modules -->
                <module id="comp_actions" class="Application.API.Class.ComponentActions" />
+               <!-- changer command modules -->
+               <module id="changer_command" class="Application.API.Class.ChangerCommand" />
        </modules>
 </configuration>
index 81e24edbe6530d376ca9dbb475c7928eb6ae344a..8988b8a3a768778732e8e98b512a1f7a9636a4d5 100644 (file)
@@ -4,5 +4,6 @@
        <url ServiceParameter="APIInstallWizard" pattern="panel/config/" />
        <url ServiceParameter="APIBasicUsers" pattern="panel/basic/" />
        <url ServiceParameter="APIOAuth2Clients" pattern="panel/oauth2/" />
+       <url ServiceParameter="APIDevices" pattern="panel/devices/" />
        <url ServiceParameter="APISettings" pattern="panel/settings/" />
 </urls>
index ff2934380a223d427078b524bee7e765e2151aed..5c1e93f876741cac05e3a13745693d4a050e0a99 100644 (file)
@@ -33,6 +33,7 @@
                <a href="<%=$this->Service->constructUrl('APIHome')%>" class="w3-bar-item w3-button w3-padding<%=$this->Service->getRequestedPagePath() == 'APIHome' ? ' w3-blue': ''%>"><i class="fas fa-tachometer-alt fa-fw"></i> &nbsp;<%[ Dashboard ]%></a>
                <a href="<%=$this->Service->constructUrl('APIBasicUsers')%>" class="w3-bar-item w3-button w3-padding<%=$this->Service->getRequestedPagePath() == 'APIBasicUsers' ? ' w3-blue': ''%>"><i class="fa fa-users fa-fw"></i> &nbsp;<%[ Basic users ]%></a>
                <a href="<%=$this->Service->constructUrl('APIOAuth2Clients')%>" class="w3-bar-item w3-button w3-padding<%=$this->Service->getRequestedPagePath() == 'APIOAuth2Clients' ? ' w3-blue': ''%>"><i class="fa fa-user-shield fa-fw"></i> &nbsp;<%[ OAuth2 clients ]%></a>
+               <a href="<%=$this->Service->constructUrl('APIDevices')%>" class="w3-bar-item w3-button w3-padding<%=$this->Service->getRequestedPagePath() == 'APIDevices' ? ' w3-blue': ''%>"><i class="fa fa-server fa-fw"></i> &nbsp;<%[ Devices ]%></a>
                <a href="<%=$this->Service->constructUrl('APISettings')%>" class="w3-bar-item w3-button w3-padding<%=$this->Service->getRequestedPagePath() == 'APISettings' ? ' w3-blue': ''%>"><i class="fa fa-wrench fa-fw"></i> &nbsp;<%[ Settings ]%></a>
                <a href="<%=$this->Service->constructUrl('APIInstallWizard')%>" class="w3-bar-item w3-button w3-padding<%=$this->Service->getRequestedPagePath() == 'APIInstallWizard' ? ' w3-blue': ''%>"><i class="fa fa-hat-wizard fa-fw"></i> &nbsp;<%[ Configuration wizard ]%></a>
        </div>
diff --git a/gui/baculum/protected/API/Portlets/SudoConfig.php b/gui/baculum/protected/API/Portlets/SudoConfig.php
new file mode 100644 (file)
index 0000000..2cb287d
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum   - Bacula web interface
+ *
+ * Copyright (C) 2013-2021 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+Prado::using('System.Web.UI.JuiControls.TJuiDialog');
+Prado::using('Application.Common.Portlets.PortletTemplate');
+
+/**
+ * Sudo config control.
+ * It displays dialog window with sudo config for specific operating systems.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @category Control
+ * @package Baculum API
+ */
+class SudoConfig extends PortletTemplate {
+}
+?>
diff --git a/gui/baculum/protected/API/Portlets/SudoConfig.tpl b/gui/baculum/protected/API/Portlets/SudoConfig.tpl
new file mode 100644 (file)
index 0000000..1ea10ce
--- /dev/null
@@ -0,0 +1,53 @@
+<script>
+var oSudoConfig = {
+       bin_fields: {},
+       bin_opts: {},
+       ids: {
+               dialog: '<%=$this->SudoConfigPopup->ClientID%>'
+       },
+       set_bin_fields: function(bin_fields) {
+               this.bin_fields = bin_fields;
+       },
+       set_bin_opts: function(bin_opts) {
+               this.bin_opts = bin_opts;
+       },
+       get_config: function(type) {
+               var val, pre;
+               var cfg = '';
+               var users = ['apache', 'lighttpd', 'www-data'];
+               var fields = this.bin_fields.hasOwnProperty(type) ? this.bin_fields[type] : [];
+               for (var i = 0; i < users.length; i++) {
+                       var pre = document.getElementById('sudo_config_' + users[i].replace(/-/g, '_'));
+                       pre.textContent = 'Defaults:' + users[i] + ' !requiretty' + "\n";
+                       for (var j = 0; j < fields.length; j++) {
+                               val = document.getElementById(fields[j]).value.trim();
+                               if (this.bin_opts.hasOwnProperty(type)) {
+                                       if (this.bin_opts[type].hasOwnProperty('base_path') && this.bin_opts[type].base_path) {
+                                               val = val.split(' ').shift(); // NOTE: It will not work with paths containing spaces
+                                       }
+                               }
+                               if (val) {
+                                       pre.textContent += users[i] + ' ALL = (root) NOPASSWD: ' + val + "\n";
+                               }
+                       }
+               }
+               $('#' + this.ids.dialog).dialog('open');
+       }
+};
+</script>
+<com:TJuiDialog
+       ID="SudoConfigPopup"
+       Options.title="<%[ Sudo configuration ]%>"
+       Options.autoOpen="false",
+       Options.minWidth="820"
+       Options.minHeight="200"
+>
+       <p><%[ Please copy appropriate sudo configuration and put it to a new sudoers.d file for example /etc/sudoers.d/baculum-api ]%></p>
+       <p><strong><%[ Note ]%></strong> <%[ Please use visudo to add this configuration, otherwise please do remember to add empty line at the end of file. ]%>
+       <p><%[ Example sudo configuration for Apache web server user (RHEL, CentOS and others): ]%></p>
+       <pre id="sudo_config_apache"></pre>
+       <p><%[ Example sudo configuration for Lighttpd web server user (RHEL, CentOS and others): ]%></p>
+       <pre id="sudo_config_lighttpd"></pre>
+       <p><%[ Example sudo configuration for Apache and Lighttpd web servers user (Debian, Ubuntu and others): ]%></p>
+       <pre id="sudo_config_www_data"></pre>
+</com:TJuiDialog>
index dc667c763ce3d460f274f907958b617fe91802ac..ce92ed0d599a80f8c2de8cb9a615c1d3110c1191 100644 (file)
@@ -31,7 +31,7 @@ Prado::using('System.Web.UI.WebControls.TClientScript');
  */
 class BClientScript extends TClientScript {
 
-       const SCRIPTS_VERSION = 17;
+       const SCRIPTS_VERSION = 18;
 
        public function getScriptUrl()
        {
index 828daa6c97a238fb3f86054eeb982abbbdbd4070..2ac2c2bf21125a81f4129bdc59baf5622c20a63f 100644 (file)
@@ -230,4 +230,21 @@ class OAuth2Error extends GenericError {
        const MSG_ERROR_OAUTH2_CLIENT_INVALID_CONSOLE = 'Invalid Console name.';
        const MSG_ERROR_OAUTH2_CLIENT_INVALID_DIRECTOR = 'Invalid Director name.';
 }
+class DeviceError extends GenericError {
+
+       const ERROR_DEVICE_DEVICE_CONFIG_DOES_NOT_EXIST = 130;
+       const ERROR_DEVICE_INVALID_COMMAND = 131;
+       const ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST = 132;
+       const ERROR_DEVICE_AUTOCHANGER_DRIVE_DOES_NOT_EXIST = 132;
+       const ERROR_DEVICE_WRONG_SLOT_NUMBER = 133;
+       const ERROR_DEVICE_DRIVE_DOES_NOT_BELONG_TO_AUTOCHANGER = 134;
+
+       const MSG_ERROR_DEVICE_DEVICE_CONFIG_DOES_NOT_EXIST = 'Device config does not exist.';
+       const MSG_ERROR_DEVICE_INVALID_COMMAND = 'Invalid changer command.';
+       const MSG_ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST = 'Autochanger does not exist.';
+       const MSG_ERROR_DEVICE_AUTOCHANGER_DRIVE_DOES_NOT_EXIST = 'Autochanger drive does not exist.';
+       const MSG_ERROR_DEVICE_WRONG_SLOT_NUMBER = 'Wrong slot number.';
+       const MSG_ERROR_DEVICE_DRIVE_DOES_NOT_BELONG_TO_AUTOCHANGER = 'Drive does not belong to selected autochanger.';
+}
+
 ?>
index 364c9cefc8f4a321aaecc29cf8184495fbb5d7fd..485b39403bb437463eab87656df51a4b336eefc2 100644 (file)
@@ -84,10 +84,10 @@ abstract class OAuth2 extends CommonModule {
        /**
         * Expiration time in seconds for access token.
         * 
-        * Temportary set to 60 seconds for testst purposes.
+        * Temportary set to 15 minutes for testst purposes.
         * In production the value SHOULD BE changed.
         */
-       const ACCESS_TOKEN_EXPIRES_TIME = 60;
+       const ACCESS_TOKEN_EXPIRES_TIME = 90000;
 
        /**
         * Scope pattern.
index 73f9ae38d295b701c41098e3825ab67bd210df53..252443cfb82e1a074d4ce4e9de0998e9970a355c 100644 (file)
@@ -206,6 +206,7 @@ var OAuth2Scopes = [
        'directors',
        'clients',
        'storages',
+       'devices',
        'volumes',
        'pools',
        'bvfs',
@@ -230,6 +231,17 @@ function copy_to_clipboard(text) {
        document.body.removeChild(input);
 }
 
+/**
+ * Used to escape values before putting them into regular expression.
+ * Dedicated to use in table values.
+ */
+dtEscapeRegex = function(value) {
+       if (typeof(value) != 'string' && typeof(value.toString) == 'function') {
+               value = value.toString();
+       }
+       return $.fn.dataTable.util.escapeRegex(value);
+};
+
 $(function() {
        W3SideBar.init();
        set_global_listeners();
index 1cb04d4b3e518e4f440ba3f2cac2dd820fec8cc6..907b866d88adf0d9713089d3214383b866fb77d6 100644 (file)
@@ -343,7 +343,11 @@ function render_jobstatus(data, type, row) {
 function render_bytes(data, type, row) {
        var s;
        if (type == 'display') {
-               s = Units.get_formatted_size(data)
+               if (/^\d+$/.test(data)) {
+                       s = Units.get_formatted_size(data);
+               } else {
+                       s = '';
+               }
        } else {
                s = data;
        }
@@ -1105,17 +1109,6 @@ function update_job_table(table_obj, new_data) {
        table_obj.page(current_page).draw(false);
 }
 
-/**
- * Used to escape values before putting them into regular expression.
- * Dedicated to use in table values.
- */
-dtEscapeRegex = function(value) {
-       if (typeof(value) != 'string' && typeof(value.toString) == 'function') {
-               value = value.toString();
-       }
-       return $.fn.dataTable.util.escapeRegex(value);
-};
-
 /**
  * Do validation comma separated list basing on regular expression
  * for particular values.
@@ -1184,9 +1177,11 @@ function get_table_toolbar(table, actions, txt) {
                        acts[select.value].before();
                }
                selected = selected.join('|');
-               acts[select.value].callback.options.RequestTimeOut = 60000; // Timeout set to 1 minute
-               acts[select.value].callback.setCallbackParameter(selected);
-               acts[select.value].callback.dispatch();
+               if (acts[select.value].hasOwnProperty('callback')) {
+                       acts[select.value].callback.options.RequestTimeOut = 60000; // Timeout set to 1 minute
+                       acts[select.value].callback.setCallbackParameter(selected);
+                       acts[select.value].callback.dispatch();
+               }
        });
        table_toolbar.appendChild(title);
        table_toolbar.appendChild(select);
index cd34cb87e57df6f67e34fa908d35e3631ec42806..44a01c253e1cd454c8e4d441bb44d39d33b41926 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/en/messages.mo and b/gui/baculum/protected/Web/Lang/en/messages.mo differ
index ad856d8decda16f2834d1bf178c627e2a3120bd0..71883eadbe54d3fca9b4efcb864d56dd2bfca7fd 100644 (file)
@@ -3208,3 +3208,96 @@ msgstr "Last %errors/%count jobs finished with error."
 
 msgid "All last %count jobs finished successfully."
 msgstr "All last %count jobs finished successfully."
+
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Drive name"
+msgstr "Drive name"
+
+msgid "Unload"
+msgstr "Unload"
+
+msgid "Manage autochanger"
+msgstr "Manage autochanger"
+
+msgid "Slot in device"
+msgstr "Slot in device"
+
+msgid "Slot in catalog"
+msgstr "Slot in catalog"
+
+msgid "Load"
+msgstr "Load"
+
+msgid "Load drive"
+msgstr "Load drive"
+
+msgid "index"
+msgstr "index"
+
+msgid "device"
+msgstr "device"
+
+msgid "Drive"
+msgstr "Drive"
+
+msgid "Empty"
+msgstr "Empty"
+
+msgid "In drive %index (%drive)"
+msgstr "In drive %index (%drive)"
+
+msgid "Import/Export"
+msgstr "Import/Export"
+
+msgid "Label using barcodes"
+msgstr "Label using barcodes"
+
+msgid "Move to import/export slot"
+msgstr "Move to import/export slot"
+
+msgid "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+msgstr  "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Slots"
+msgstr "Slots"
+
+msgid "Release I/E"
+msgstr "Release I/E"
+
+msgid "Move to import/export slots"
+msgstr "Move to import/export slots"
+
+msgid "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+msgstr "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+
+msgid "Import/export slot"
+msgstr "Import/export slot"
+
+msgid "Destination slot"
+msgstr "Destination slot"
+
+msgid "Release import/export slot"
+msgstr "Release import/export slot"
+
+msgid "Tape drives"
+msgstr "Tape drives"
+
+msgid "Changer slots"
+msgstr "Changer slots"
+
+msgid "Release all I/E slots"
+msgstr "Release all I/E slots"
+
+msgid "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+msgstr "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+
+msgid "Tip: To use bulk autochanger actions, please select table rows."
+msgstr "Tip: To use bulk autochanger actions, please select table rows."
+
+msgid "Mount volume"
+msgstr "Mount volume"
index fa72c70d854ff8fa0c41db89f2bcf66b2d14567d..a039df50c49c63b13a2d00e232b21948790f3d3f 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/ja/messages.mo and b/gui/baculum/protected/Web/Lang/ja/messages.mo differ
index f69f6561ee73ae6a58660b278adf9ee803ee90c5..3893d5868f986c0d93672b237552b4bb066902a4 100644 (file)
@@ -3294,3 +3294,96 @@ msgstr "Last %errors/%count jobs finished with error."
 
 msgid "All last %count jobs finished successfully."
 msgstr "All last %count jobs finished successfully."
+
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Drive name"
+msgstr "Drive name"
+
+msgid "Unload"
+msgstr "Unload"
+
+msgid "Manage autochanger"
+msgstr "Manage autochanger"
+
+msgid "Slot in device"
+msgstr "Slot in device"
+
+msgid "Slot in catalog"
+msgstr "Slot in catalog"
+
+msgid "Load"
+msgstr "Load"
+
+msgid "Load drive"
+msgstr "Load drive"
+
+msgid "index"
+msgstr "index"
+
+msgid "device"
+msgstr "device"
+
+msgid "Drive"
+msgstr "Drive"
+
+msgid "Empty"
+msgstr "Empty"
+
+msgid "In drive %index (%drive)"
+msgstr "In drive %index (%drive)"
+
+msgid "Import/Export"
+msgstr "Import/Export"
+
+msgid "Label using barcodes"
+msgstr "Label using barcodes"
+
+msgid "Move to import/export slot"
+msgstr "Move to import/export slot"
+
+msgid "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+msgstr  "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Slots"
+msgstr "Slots"
+
+msgid "Release I/E"
+msgstr "Release I/E"
+
+msgid "Move to import/export slots"
+msgstr "Move to import/export slots"
+
+msgid "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+msgstr "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+
+msgid "Import/export slot"
+msgstr "Import/export slot"
+
+msgid "Destination slot"
+msgstr "Destination slot"
+
+msgid "Release import/export slot"
+msgstr "Release import/export slot"
+
+msgid "Tape drives"
+msgstr "Tape drives"
+
+msgid "Changer slots"
+msgstr "Changer slots"
+
+msgid "Release all I/E slots"
+msgstr "Release all I/E slots"
+
+msgid "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+msgstr "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+
+msgid "Tip: To use bulk autochanger actions, please select table rows."
+msgstr "Tip: To use bulk autochanger actions, please select table rows."
+
+msgid "Mount volume"
+msgstr "Mount volume"
index 3ff773a624ea5a2fafa0dee6d673642683bb946a..3ae9096cedb462ea17959452df369726197512b2 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/pl/messages.mo and b/gui/baculum/protected/Web/Lang/pl/messages.mo differ
index 4553201b049d84eaca7e26e5c98c6d55adeccc64..64735fd310207429b77b93b7f4ff1a6ad0bdd658 100644 (file)
@@ -3219,3 +3219,95 @@ msgstr "Ostatnie %errors/%count zadań zakończyło się błędem."
 msgid "All last %count jobs finished successfully."
 msgstr "Wszystkie ostatnie %count zadań zakończyło się pomyślnie."
 
+msgid "Drive index"
+msgstr "Indeks napędu"
+
+msgid "Drive name"
+msgstr "Nazwa napędu"
+
+msgid "Unload"
+msgstr "Wyładuj"
+
+msgid "Manage autochanger"
+msgstr "Zarządzaj zmieniarką taśm"
+
+msgid "Slot in device"
+msgstr "Slot w urządzeniu"
+
+msgid "Slot in catalog"
+msgstr "Slot w bazie danych"
+
+msgid "Load"
+msgstr "Załaduj"
+
+msgid "Load drive"
+msgstr "Załaduj napęd"
+
+msgid "index"
+msgstr "indeks"
+
+msgid "device"
+msgstr "urządzenie"
+
+msgid "Drive"
+msgstr "Napęd"
+
+msgid "Empty"
+msgstr "Pusty"
+
+msgid "In drive %index (%drive)"
+msgstr "W napędzie %index (%drive)"
+
+msgid "Import/Export"
+msgstr "Import/Eksport"
+
+msgid "Label using barcodes"
+msgstr "Etykietuj używając kodów kreskowych"
+
+msgid "Move to import/export slot"
+msgstr "Przenieś do slotu import/eksport"
+
+msgid "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+msgstr  "Zarządzanie zmieniarką taśm jest niedostępne. W celu zarządzania zmieniarką taśm z tego miejsce, dodaj ją do urządzeń hosta API po stronie hosta API."
+
+msgid "Drive index:"
+msgstr "Indeks napędu:"
+
+msgid "Slots"
+msgstr "Sloty"
+
+msgid "Release I/E"
+msgstr "Zwolnij I/E"
+
+msgid "Move to import/export slots"
+msgstr "Przenieś do slotów import/eksport"
+
+msgid "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+msgstr "Jest za mało slotów import/eksport do przetrasferowania wybranych wolumenów. Ilość wolnych slotów import/eksport: %slots_count, ilość wybranych wolumenów: %vols_count."
+
+msgid "Import/export slot"
+msgstr "Sloty import/eksport"
+
+msgid "Destination slot"
+msgstr "Docelowy slot"
+
+msgid "Release import/export slot"
+msgstr "Zwolnij slot import/eksport"
+
+msgid "Tape drives"
+msgstr "Napędy taśmowe"
+
+msgid "Changer slots"
+msgstr "Sloty zmieniarki"
+
+msgid "Release all I/E slots"
+msgstr "Zwolnij wszystkie sloty I/E"
+
+msgid "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+msgstr "Jest za mało regularnych slotów do przetrasferowania wybranych wolumenów ze slotów import/eksport. Ilość pełnych slotów import/eksport: %ie_slot_count, ilość wolnych regularnych slotów: %slot_count."
+
+msgid "Tip: To use bulk autochanger actions, please select table rows."
+msgstr "Wskazówka: W celu użycia akcji zbiorczych zmieniarki taśm, proszę zaznaczyć wiersze tabeli."
+
+msgid "Mount volume"
+msgstr "Montuj wolumen"
index f5894b7f7af0da36211a46b55d6e532638bb277b..aacd488f77a102f4c1bc8e8360c838999125a8e6 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/pt/messages.mo and b/gui/baculum/protected/Web/Lang/pt/messages.mo differ
index 057f5c787d53af28992368f159c5d29541138cf7..f9948d7961dbbea79ea27a96b43cacc994c3a450 100644 (file)
@@ -3219,3 +3219,95 @@ msgstr "Últimos %errors/%count jobs concluídos com erro."
 msgid "All last %count jobs finished successfully."
 msgstr "Todos os últimos %count jobs foram concluídos com sucesso."
 
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Drive name"
+msgstr "Drive name"
+
+msgid "Unload"
+msgstr "Unload"
+
+msgid "Manage autochanger"
+msgstr "Manage autochanger"
+
+msgid "Slot in device"
+msgstr "Slot in device"
+
+msgid "Slot in catalog"
+msgstr "Slot in catalog"
+
+msgid "Load"
+msgstr "Load"
+
+msgid "Load drive"
+msgstr "Load drive"
+
+msgid "index"
+msgstr "index"
+
+msgid "device"
+msgstr "device"
+
+msgid "Drive"
+msgstr "Drive"
+
+msgid "Empty"
+msgstr "Empty"
+
+msgid "In drive %index (%drive)"
+msgstr "In drive %index (%drive)"
+
+msgid "Import/Export"
+msgstr "Import/Export"
+
+msgid "Label using barcodes"
+msgstr "Label using barcodes"
+
+msgid "Move to import/export slot"
+msgstr "Move to import/export slot"
+
+msgid "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+msgstr  "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Slots"
+msgstr "Slots"
+
+msgid "Release I/E"
+msgstr "Release I/E"
+
+msgid "Move to import/export slots"
+msgstr "Move to import/export slots"
+
+msgid "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+msgstr "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+
+msgid "Import/export slot"
+msgstr "Import/export slot"
+
+msgid "Destination slot"
+msgstr "Destination slot"
+
+msgid "Release import/export slot"
+msgstr "Release import/export slot"
+
+msgid "Tape drives"
+msgstr "Tape drives"
+
+msgid "Changer slots"
+msgstr "Changer slots"
+
+msgid "Release all I/E slots"
+msgstr "Release all I/E slots"
+
+msgid "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+msgstr "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+
+msgid "Tip: To use bulk autochanger actions, please select table rows."
+msgstr "Tip: To use bulk autochanger actions, please select table rows."
+
+msgid "Mount volume"
+msgstr "Mount volume"
index ae003a8176345ce206b318ead176cd01c8fd17c9..7f7d5d2efae379d017dbd4268a2fc3632a43baae 100644 (file)
Binary files a/gui/baculum/protected/Web/Lang/ru/messages.mo and b/gui/baculum/protected/Web/Lang/ru/messages.mo differ
index 5006c0da8d81c66b4dec56b01e7f086ac95be354..0b6922409568645f0923da4f1e0925a5785714d8 100644 (file)
@@ -3219,3 +3219,95 @@ msgstr "Последнее %errors/%count заданий закончились
 msgid "All last %count jobs finished successfully."
 msgstr "Все последние %count задания успешно завершены."
 
+msgid "Drive index"
+msgstr "Drive index"
+
+msgid "Drive name"
+msgstr "Drive name"
+
+msgid "Unload"
+msgstr "Unload"
+
+msgid "Manage autochanger"
+msgstr "Manage autochanger"
+
+msgid "Slot in device"
+msgstr "Slot in device"
+
+msgid "Slot in catalog"
+msgstr "Slot in catalog"
+
+msgid "Load"
+msgstr "Load"
+
+msgid "Load drive"
+msgstr "Load drive"
+
+msgid "index"
+msgstr "index"
+
+msgid "device"
+msgstr "device"
+
+msgid "Drive"
+msgstr "Drive"
+
+msgid "Empty"
+msgstr "Empty"
+
+msgid "In drive %index (%drive)"
+msgstr "In drive %index (%drive)"
+
+msgid "Import/Export"
+msgstr "Import/Export"
+
+msgid "Label using barcodes"
+msgstr "Label using barcodes"
+
+msgid "Move to import/export slot"
+msgstr "Move to import/export slot"
+
+msgid "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+msgstr  "Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side."
+
+msgid "Drive index:"
+msgstr "Drive index:"
+
+msgid "Slots"
+msgstr "Slots"
+
+msgid "Release I/E"
+msgstr "Release I/E"
+
+msgid "Move to import/export slots"
+msgstr "Move to import/export slots"
+
+msgid "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+msgstr "There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count."
+
+msgid "Import/export slot"
+msgstr "Import/export slot"
+
+msgid "Destination slot"
+msgstr "Destination slot"
+
+msgid "Release import/export slot"
+msgstr "Release import/export slot"
+
+msgid "Tape drives"
+msgstr "Tape drives"
+
+msgid "Changer slots"
+msgstr "Changer slots"
+
+msgid "Release all I/E slots"
+msgstr "Release all I/E slots"
+
+msgid "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+msgstr "There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count."
+
+msgid "Tip: To use bulk autochanger actions, please select table rows."
+msgstr "Tip: To use bulk autochanger actions, please select table rows."
+
+msgid "Mount volume"
+msgstr "Mount volume"
index ce24a2a4b9246e9c4520ef079b4b68891b6828c4..b29b26a334528ab3cf66848284cbd76b332d8bdf 100644 (file)
                        Visible="<%=!empty($_SESSION['sd']) && $this->getIsAutochanger()%>"
                        OnClick="setAutochanger"
                />
+               <com:TActiveLinkButton
+                       CssClass="w3-bar-item w3-button tab_btn"
+                       Attributes.onclick="W3Tabs.open(this.id, 'manage_autochanger');"
+                       Text="<%[ Manage autochanger ]%>"
+                       Visible="<%=!empty($_SESSION['sd']) && $this->getIsAutochanger()%>"
+                       OnCallback="loadAutochanger"
+               />
        </div>
        <div class="w3-container tab_item" id="storage_actions">
                <com:TValidationSummary
@@ -59,8 +66,7 @@
                        CssClass="w3-button w3-green w3-margin-bottom"
                        ValidationGroup="AutoChangerGroup"
                        CausesValidation="true"
-                       ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();$('#storage_action_text_output').slideDown();"
+                       ClientSide.OnLoading="oStorageActions.show_loader(true);"
                >
                        <prop:Text><%=Prado::localize('Mount')%> &nbsp;<i class="fa fa-caret-down"></i></prop:Text>
                </com:TActiveLinkButton>
@@ -70,8 +76,7 @@
                        CssClass="w3-button w3-green w3-margin-bottom"
                        ValidationGroup="AutoChangerGroup"
                        CausesValidation="true"
-                       ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();$('#storage_action_text_output').slideDown();"
+                       ClientSide.OnLoading="oStorageActions.show_loader(true);"
                >
                        <prop:Text><%=Prado::localize('Release')%> &nbsp;<i class="fa fa-caret-right"></i></prop:Text>
                </com:TActiveLinkButton>
                        CssClass="w3-button w3-green w3-margin-bottom"
                        ValidationGroup="AutoChangerGroup"
                        CausesValidation="true"
-                       ClientSide.OnLoading="$('#status_storage_loading').show();"
-                       ClientSide.OnSuccess="$('#status_storage_loading').hide();$('#storage_action_text_output').slideDown();"
+                       ClientSide.OnLoading="oStorageActions.show_loader(true);"
                >
                        <prop:Text><%=Prado::localize('Umount')%> &nbsp;<i class="fa fa-caret-up"></i></prop:Text>
                </com:TActiveLinkButton>
+               <com:TCallback ID="MountLoading" OnCallback="mountLoading" />
+               <com:TCallback ID="ReleaseLoading" OnCallback="releaseLoading" />
+               <com:TCallback ID="UmountLoading" OnCallback="umountLoading" />
+               <script>
+var oStorageActions = {
+       ids: {
+               log: 'storage_action_log',
+               log_container: 'storage_action_text_output',
+               loader: 'status_storage_loading'
+       },
+       refresh_mount: function(out_id) {
+               setTimeout(function() {
+                       var cb = <%=$this->MountLoading->ActiveControl->Javascript%>;
+                       cb.setCallbackParameter(out_id);
+                       cb.dispatch();
+               }, 2000);
+       },
+       refresh_release: function(out_id) {
+               setTimeout(function() {
+                       var cb = <%=$this->ReleaseLoading->ActiveControl->Javascript%>;
+                       cb.setCallbackParameter(out_id);
+                       cb.dispatch();
+               }, 2000);
+       },
+       refresh_umount: function(out_id) {
+               setTimeout(function() {
+                       var cb = <%=$this->UmountLoading->ActiveControl->Javascript%>;
+                       cb.setCallbackParameter(out_id);
+                       cb.dispatch();
+               }, 2000);
+       },
+       log: function(output) {
+               var log = document.getElementById(oStorageActions.ids.log);
+               log.textContent = output;
+       },
+       clear_log: function() {
+               document.getElementById(oStorageActions.ids.log_container).style.display = 'none';
+               oStorageActions.log('');
+       },
+       show_loader: function(show) {
+               var loader = document.getElementById(oStorageActions.ids.loader);
+               if (show) {
+                       oStorageActions.clear_log();
+                       $('#' + oStorageActions.ids.log_container).slideDown();
+                       loader.style.display = '';
+               } else {
+                       loader.style.display = 'none';
+               }
+       }
+};
+               </script>
                <i id="status_storage_loading" class="fa fa-sync w3-spin" style="display: none; vertical-align: super;"></i> <span id="status_storage_error" class="w3-text-red" style="display: none"></span>
                <com:TActivePanel ID="Autochanger" Display="None" Height="61px">
                                <div class="w3-left w3-margin-right">
-                                               <label><%[ Drive number: ]%></label>
-                                               <com:TActiveTextBox ID="Drive" AutoPostBack="false" Text="0" MaxLength="3" CssClass="w3-input smallbox" />
-                                               <com:TDataTypeValidator ID="DriveValidator" ValidationGroup="AutoChangerGroup" ControlToValidate="Drive" ErrorMessage="<%[ Drive number must be integer. ]%>" Display="None" DataType="Integer" />
+                                       <label><%[ Drive number: ]%></label>
+                                       <com:TActiveTextBox
+                                               ID="Drive"
+                                               AutoPostBack="false"
+                                               Text="0"
+                                               MaxLength="3"
+                                               CssClass="w3-input smallbox"
+                                       />
+                                       <com:TDataTypeValidator
+                                               ID="DriveValidator"
+                                               ValidationGroup="AutoChangerGroup"
+                                               ControlToValidate="Drive"
+                                               ErrorMessage="<%[ Drive number must be integer. ]%>"
+                                               Display="None"
+                                               DataType="Integer"
+                                       />
                                </div>
                                <div class="w3-left">
                                        <%[ Slot number: ]%>
-                                       <com:TActiveTextBox ID="Slot" AutoPostBack="false" Text="0" MaxLength="3" CssClass="w3-input smallbox" />
-                                       <com:TDataTypeValidator ID="SlotValidator" ValidationGroup="AutoChangerGroup" ControlToValidate="Slot" ErrorMessage="<%[ Slot number must be integer. ]%>" Display="None" DataType="Integer" />
+                                       <com:TActiveTextBox
+                                               ID="Slot"
+                                               AutoPostBack="false"
+                                               Text="0"
+                                               MaxLength="3"
+                                               CssClass="w3-input smallbox"
+                                       />
+                                       <com:TDataTypeValidator
+                                               ID="SlotValidator"
+                                               ValidationGroup="AutoChangerGroup"
+                                               ControlToValidate="Slot"
+                                               ErrorMessage="<%[ Slot number must be integer. ]%>"
+                                               Display="None"
+                                               DataType="Integer"
+                                       />
                                </div>
                </com:TActivePanel>
                <div id="storage_action_text_output" class="w3-code" style="display: none; clear: both;">
-                       <pre><com:TActiveLabel ID="StorageActionLog" /></pre>
+                       <pre id="storage_action_log"></pre>
                </div>
                <div id="status_storage_container" class="w3-clear w3-margin-top" style="display: none">
                        <div class="w3-right w3-margin-top w3-margin-right" title="<%[ To disable refreshing please type 0. ]%>">
@@ -1559,4 +1640,920 @@ MonitorParams = {
                        ShowCancelButton="false"
                />
        </div>
+       <div class="w3-container tab_item" id="manage_autochanger" style="display: none">
+               <div id="manage_autochanger_not_available">
+                       <strong><%[ Autochanger management is unavailable. To manage autochanger from here, add it to the API host devices on the API host side. ]%></strong>
+               </div>
+               <div id="drive_list_container">
+                       <h5><%[ Tape drives ]%></h5>
+                       <table id="drive_list" class="w3-table w3-striped w3-hoverable w3-white w3-margin-bottom" style="width: 100%">
+                               <thead>
+                                       <tr>
+                                               <th></th>
+                                               <th><%[ Drive index ]%></th>
+                                               <th><%[ Drive name ]%></th>
+                                               <th><%[ Device ]%></th>
+                                               <th><%[ Volume ]%></th>
+                                               <th><%[ Slot ]%></th>
+                                               <th><%[ Actions ]%></th>
+                                       </tr>
+                               </thead>
+                               <tbody id="drive_list_body"></tbody>
+                               <tfoot>
+                                       <tr>
+                                               <th></th>
+                                               <th><%[ Drive index ]%></th>
+                                               <th><%[ Drive name ]%></th>
+                                               <th><%[ Device ]%></th>
+                                               <th><%[ Volume ]%></th>
+                                               <th><%[ Slot ]%></th>
+                                               <th><%[ Actions ]%></th>
+                                       </tr>
+                               </tfoot>
+                       </table>
+               </div>
+               <com:TCallback ID="UnloadDrive" OnCallback="unloadDrive" />
+               <com:TCallback ID="UnloadedDrive" OnCallback="unloadedDrive" />
+               <script>
+var oDriveList = {
+       ids: {
+               drive_list: 'drive_list',
+               drive_list_body: 'drive_list_body'
+       },
+       table: null,
+       data: [],
+       init: function() {
+               if (!this.table) {
+                       this.set_table();
+               } else {
+                       var page = this.table.page();
+                       this.table.clear().rows.add(this.data).draw();
+                       this.table.page(page).draw(false);
+               }
+       },
+       set_table: function() {
+               this.table = $('#' + this.ids.drive_list).DataTable({
+                       data: this.data,
+                       deferRender: true,
+                       dom: 'lBfrtip',
+                       stateSave: true,
+                       buttons: [
+                               'copy', 'csv', 'colvis'
+                       ],
+                       columns: [
+                               {
+                                       className: 'details-control',
+                                       orderable: false,
+                                       data: null,
+                                       defaultContent: '<button type="button" class="w3-button w3-blue"><i class="fa fa-angle-down"></i></button>'
+                               },
+                               {data: 'index'},
+                               {data: 'drive'},
+                               {data: 'device'},
+                               {
+                                       data: 'volume',
+                                       render: function(data, type, row) {
+                                               var v = data;
+                                               if (type == 'display' && row.mediaid > 0) {
+                                                       var a = document.createElement('A');
+                                                       a.href = '/web/volume/' + row.mediaid + '/';
+                                                       var icon = document.createElement('I');
+                                                       icon.className  = 'fa fa-external-link-alt fa-xs';
+                                                       a.appendChild(icon);
+                                                       v += ' ' + a.outerHTML;
+                                               }
+                                               return v;
+                                       }
+                               },
+                               {data: 'slot_ach'},
+                               {
+                                       data: 'slot_ach',
+                                       render: function(data, type, row) {
+                                               var btn = document.createElement('BUTTON');
+                                               btn.className = 'w3-button w3-green';
+                                               btn.type = 'button';
+                                               var i = document.createElement('I');
+                                               i.className = 'fa fa-upload';
+                                               var label = document.createTextNode(' <%[ Unload ]%>');
+                                               btn.appendChild(i);
+                                               btn.innerHTML += '&nbsp';
+                                               btn.appendChild(label);
+                                               btn.setAttribute('onclick', 'oDrives.unload_drive("' + row.drive + '", ' + data + ')');
+                                               if (data === '')  {
+                                                       btn.style.visibility = 'hidden';
+                                               }
+                                               return btn.outerHTML;
+                                       }
+                               }
+                       ],
+                       responsive: {
+                               details: {
+                                       type: 'column'
+                               }
+                       },
+                       columnDefs: [{
+                               className: 'control',
+                               orderable: false,
+                               targets: 0
+                       },
+                       {
+                               className: "dt-center",
+                               targets: [ 1, 5, 6 ]
+                       }],
+                       order: [1, 'asc']
+               });
+       }
+};
+
+var oDrives = {
+       unload_drive_timeout: 300000, // 5 minutes
+       load_drives_cb: function(data) {
+               oDriveList.data = data;
+               oDriveList.init();
+       },
+       unload_drive: function(drive, slot) {
+               var cb = <%=$this->UnloadDrive->ActiveControl->Javascript%>;
+               var param = {
+                       drive: drive,
+                       slot: slot
+               };
+               cb.setCallbackParameter(param);
+               cb.setRequestTimeOut(this.unload_drive_timeout);
+               cb.dispatch();
+               oSlots.show_changer_loader(true);
+       },
+       set_drive_unloading_output: function(out_id) {
+               var cb = <%=$this->UnloadedDrive->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(out_id);
+               cb.dispatch();
+       },
+       refresh_drive_unloading: function(out_id) {
+               setTimeout(function() {
+                       oDrives.set_drive_unloading_output(out_id);
+               }, 2000)
+       }
+};
+               </script>
+               <div id="slot_list_container">
+                       <h5 class="w3-show-inline-block"><%[ Changer slots ]%> &nbsp;<i id="changer_loader" class="fas fa-sync-alt w3-spin" style="display: none"></i></h5>
+                       <p class="info w3-right"><%[ Tip: To use bulk autochanger actions, please select table rows. ]%></p>
+                       <button type="button" id="release_all_ie_btn" class="w3-button w3-green" style="display: none" onclick="oSlots.release_all_ie();">
+                               <i class="fas fa-share-square"></i> &nbsp;<%[ Release all I/E slots ]%>
+                       </button>
+                       <table id="slot_list" class="w3-table w3-striped w3-hoverable w3-white w3-margin-bottom" style="width: 100%">
+                               <thead>
+                                       <tr>
+                                               <th></th>
+                                               <th><%[ Slot in device ]%></th>
+                                               <th><%[ Slot in catalog ]%></th>
+                                               <th><%[ Volume ]%></th>
+                                               <th><%[ Vol. status ]%></th>
+                                               <th><%[ Vol. bytes ]%></th>
+                                               <th><%[ Pool ]%></th>
+                                               <th>MediaType</th>
+                                               <th><%[ Last written ]%></th>
+                                               <th><%[ When expire ]%></th>
+                                               <th><%[ Actions ]%></th>
+                                       </tr>
+                               </thead>
+                               <tbody id="slot_list_body"></tbody>
+                               <tfoot>
+                                       <tr>
+                                               <th></th>
+                                               <th><%[ Slot in device ]%></th>
+                                               <th><%[ Slot in catalog ]%></th>
+                                               <th><%[ Volume ]%></th>
+                                               <th><%[ Vol. status ]%></th>
+                                               <th><%[ Vol. bytes ]%></th>
+                                               <th><%[ Pool ]%></th>
+                                               <th>MediaType</th>
+                                               <th><%[ Last written ]%></th>
+                                               <th><%[ When expire ]%></th>
+                                               <th><%[ Actions ]%></th>
+                                       </tr>
+                               </tfoot>
+                       </table>
+                       <p class="info w3-hide-medium w3-hide-small"><%[ Tip: Use left-click to select table row. Use CTRL + left-click to multiple row selection. Use SHIFT + left-click to add a range of rows to selection. ]%></p>
+               </div>
+               <com:TCallback ID="LabelUsingBarcodesAction" OnCallback="labelBarcodes" />
+               <com:TCallback ID="UpdateSlotsBarcodesAction" OnCallback="updateSlotsBarcodes" />
+               <com:TCallback ID="UpdateSlotsAction" OnCallback="updateSlots" />
+               <com:TCallback ID="MoveToImportExportSlot" OnCallback="moveToIE" />
+               <com:TCallback ID="MovingToImportExportSlot" OnCallback="movingToIE" />
+               <com:TCallback ID="MoveFromImportExportSlot" OnCallback="moveFromIE" />
+               <com:TCallback ID="MovingFromImportExportSlot" OnCallback="movingFromIE" />
+               <com:TCallback ID="ReleaseImportExportSlot" OnCallback="releaseIE" />
+               <com:TCallback ID="ReleasingImportExportSlot" OnCallback="releasingIE" />
+               <com:Application.Web.Portlets.LabelVolume
+                       ID="LabelBarcodes"
+                       ShowButton="false"
+                       BarcodeLabel="true"
+                       Storage="<%=$this->StorageName%>"
+                       OnLabelStart="showChangerLoading"
+                       OnLabelComplete="loadAutochanger"
+                       OnLabelSuccess="hideChangerLoading"
+                       OnLabelFail="hideChangerLoading"
+               />
+               <com:Application.Web.Portlets.UpdateSlots
+                       ID="UpdateSlots"
+                       ShowButton="false"
+                       Storage="<%=$this->StorageName%>"
+                       OnUpdateStart="showChangerLoading"
+                       OnUpdateComplete="loadAutochanger"
+                       OnUpdateSuccess="hideChangerLoading"
+                       OnUpdateFail="hideChangerLoading"
+               />
+               <com:TActiveHiddenField ID="IESlots" />
+               <script>
+var oSlotList = {
+       table_toolbar: null,
+       actions: [
+               {
+                       action: 'label',
+                       label: '<%[ Label using barcodes ]%>',
+                       value: 'slot_ach',
+                       callback: <%=$this->LabelUsingBarcodesAction->ActiveControl->Javascript%>,
+                       before: function() {
+                               show_label_volume_window(true);
+                       }
+               },
+               {
+                       action: 'move_to_ie',
+                       label: '<%[ Move to import/export slot ]%>',
+                       value: 'slot_ach',
+                       before: function() {
+                               oSlots.show_move_to_ie_window(true);
+                       },
+                       validate: function(selected) {
+                               oSlots.set_move_to_ie_slots();
+                               var ie_slots = oSlots.get_slots(['ie_slot'], ['E']);
+                               var ie_slots_count = ie_slots.length;
+                               var vols_count =  oSlots.move_to_ie_slots.length;
+                               if (vols_count > ie_slots_count) {
+                                       var emsg = '<%[ There are to few import/export slots to transfer selected volumes. Free import/export slots count: %slots_count, selected volumes count: %vols_count. ]%>';
+                                       emsg = emsg.replace('%slots_count', ie_slots_count);
+                                       emsg = emsg.replace('%vols_count', vols_count);
+                                       oBulkActionsModal.set_error(emsg);
+                                       return false;
+                               }
+                               return true;
+                       }
+               },
+               {
+                       action: 'update_barcodes',
+                       label: '<%[ Update slots using barcodes ]%>',
+                       value: 'slot_ach',
+                       callback: <%=$this->UpdateSlotsBarcodesAction->ActiveControl->Javascript%>,
+                       before: function() {
+                               set_update_slots_barcodes(true);
+                               show_update_slots_window(true);
+                       }
+               },
+               {
+                       action: 'update',
+                       label: '<%[ Update slots ]%>',
+                       value: 'slot_ach',
+                       callback: <%=$this->UpdateSlotsAction->ActiveControl->Javascript%>,
+                       before: function() {
+                               set_update_slots(true);
+                               show_update_slots_window(true);
+                       }
+               }
+       ],
+       ids: {
+               slot_list: 'slot_list',
+               slot_list_body: 'slot_list_body',
+               release_all_ie_btn: 'release_all_ie_btn'
+       },
+       table: null,
+       data: [],
+       init: function() {
+               if (!this.table) {
+                       this.set_table();
+                       this.set_bulk_actions();
+                       this.init_release_all_ie_btn();
+                       this.set_events();
+               } else {
+                       var page = this.table.page();
+                       this.table.clear().rows.add(this.data).draw();
+                       this.table.page(page).draw(false);
+               }
+               oSlots.init();
+               this.set_release_all_ie_btn();
+       },
+       set_events: function() {
+               document.getElementById(this.ids.slot_list).addEventListener('click', function(e) {
+                       $(function() {
+                               this.table_toolbar.style.display = this.table.rows({selected: true}).data().length > 0 ? '' : 'none';
+                       }.bind(this));
+               }.bind(this));
+       },
+       set_table: function() {
+               this.table = $('#' + this.ids.slot_list).DataTable({
+                       data: this.data,
+                       pageLength: 100,
+                       deferRender: true,
+                       dom: 'lB<"table_toolbar">frtip',
+                       stateSave: true,
+                       buttons: [
+                               'copy', 'csv', 'colvis'
+                       ],
+                       columns: [
+                               {
+                                       className: 'details-control',
+                                       orderable: false,
+                                       data: null,
+                                       defaultContent: '<button type="button" class="w3-button w3-blue"><i class="fa fa-angle-down"></i></button>'
+                               },
+                               {data: 'slot_ach'},
+                               {data: 'slot_cat'},
+                               {
+                                       data: 'volume',
+                                       render: function(data, type, row) {
+                                               var v = data;
+                                               var link = '';
+                                               if (!row.slot_cat) {
+                                                       for (var i = 0; i < oDriveList.data.length; i++) {
+                                                               if (row.slot_ach === oDriveList.data[i].slot_ach) {
+                                                                       v = '<%[ In drive %index (%drive) ]%>';
+                                                                       v = v.replace('%index', oDriveList.data[i].index);
+                                                                       v = v.replace('%drive', oDriveList.data[i].drive);
+                                                                       break;
+                                                               }
+                                                       }
+                                               }
+                                               if (type == 'display' && row.mediaid > 0) {
+                                                       var a = document.createElement('A');
+                                                       a.href = '/web/volume/' + row.mediaid + '/';
+                                                       var icon = document.createElement('I');
+                                                       icon.className  = 'fa fa-external-link-alt fa-xs';
+                                                       a.appendChild(icon);
+                                                       link = ' ' + a.outerHTML;
+                                               }
+                                               if (!data && !v) {
+                                                       if (row.state == 'F') {
+                                                               v = '<%[ Full ]%>';
+                                                       } else if (row.state == 'E') {
+                                                               v = '<%[ Empty ]%>';
+                                                       }
+                                               }
+                                               return (v + link);
+                                       }
+                               },
+                               {data: 'volstatus'},
+                               {
+                                       data: 'volbytes',
+                                       render: render_bytes
+                               },
+                               {data: 'pool'},
+                               {data: 'mediatype'},
+                               {
+                                       data: 'lastwritten',
+                                       render: render_date
+                               },
+                               {
+                                       data: 'whenexpire',
+                                       render: render_date_ex
+                               },
+                               {
+                                       data: 'slot_ach',
+                                       render: function(data, type, row) {
+                                               var ret = '';
+                                               if (row.type == 'ie_slot')  {
+                                                       if (row.state === 'F') {
+                                                               var clear_io = document.createElement('BUTTON');
+                                                               clear_io.className = 'w3-button w3-green';
+                                                               clear_io.type = 'button';
+                                                               var i = document.createElement('I');
+                                                               i.className = 'fa fa-sign-out-alt';
+                                                               var label = document.createTextNode(' <%[ Release I/E ]%>');
+                                                               clear_io.appendChild(i);
+                                                               clear_io.innerHTML += '&nbsp';
+                                                               clear_io.appendChild(label);
+                                                               clear_io.setAttribute('onclick', 'oSlots.release_ie_window(' + data + ');');
+                                                               ret = clear_io.outerHTML;
+                                                       } else {
+                                                               ret = '<%[ Import/Export ]%>';
+                                                       }
+                                               } else {
+                                                       var load_btn = document.createElement('BUTTON');
+                                                       load_btn.className = 'w3-button w3-green';
+                                                       load_btn.type = 'button';
+                                                       var i = document.createElement('I');
+                                                       i.className = 'fa fa-download';
+                                                       var label = document.createTextNode(' <%[ Load ]%>');
+                                                       load_btn.appendChild(i);
+                                                       load_btn.innerHTML += '&nbsp';
+                                                       load_btn.appendChild(label);
+                                                       load_btn.setAttribute('onclick', 'oSlots.load_drive_window(' + data + ');');
+                                                       if (row.state === 'E')  {
+                                                               load_btn.style.visibility = 'hidden';
+                                                       }
+                                                       ret = load_btn.outerHTML;
+                                               }
+                                               return ret;
+                                       }
+                               }
+                       ],
+                       responsive: {
+                               details: {
+                                       type: 'column'
+                               }
+                       },
+                       columnDefs: [{
+                               className: 'control',
+                               orderable: false,
+                               targets: 0
+                       },
+                       {
+                               className: "dt-right",
+                               targets: [ 5 ]
+                       },
+                       {
+                               className: "dt-center",
+                               targets: [ 1, 2, 4 ]
+                       }],
+                       select: {
+                               style:    'os',
+                               selector: 'td:not(:last-child):not(:first-child)',
+                               blurable: false
+                       },
+                       order: [1, 'asc']
+               });
+       },
+       set_bulk_actions: function() {
+               this.table_toolbar = get_table_toolbar(this.table, this.actions, {
+                       actions: '<%[ Actions ]%>',
+                       ok: '<%[ OK ]%>'
+               });
+       },
+       init_release_all_ie_btn: function() {
+               var btn = document.getElementById(this.ids.release_all_ie_btn);
+               var toolbar = document.querySelector('#' + this.ids.slot_list + '_wrapper div.dt-buttons');
+               toolbar.appendChild(btn);
+       },
+       show_release_all_ie_btn: function(show) {
+               var btn = document.getElementById(this.ids.release_all_ie_btn);
+               if (show) {
+                       btn.style.display = '';
+               }  else {
+                       btn.style.display = 'none';
+               }
+       },
+       set_release_all_ie_btn: function() {
+               var full = oSlots.get_slots(['ie_slot'], ['F']);
+               if (full.length > 0) {
+                       this.show_release_all_ie_btn(true);
+               } else {
+                       this.show_release_all_ie_btn(false);
+               }
+       }
+};
+
+var oSlots = {
+       ids: {
+               load_drive_window: 'load_drive_window',
+               load_drive_drives: 'load_drive_drives',
+               load_drive_out: 'load_drive_out',
+               load_drive_loading: 'load_drive_loading',
+               load_drive_slot: 'load_drive_slot',
+               move_to_ie_window: 'move_to_ie_window',
+               move_to_ie_slots: 'move_to_ie_slots',
+               move_to_ie_window_log: 'move_to_ie_window_log',
+               move_to_ie_loading: 'move_to_ie_loading',
+               release_ie_window: 'release_ie_window',
+               release_ie_slot: 'release_ie_slot',
+               release_ie_loading: 'release_ie_loading',
+               release_ie_window_log: 'release_ie_window_log',
+               release_ie_dest_slot: 'release_ie_dest_slot',
+               changer_loader: 'changer_loader'
+       },
+       load_drive_timeout: 300000, // 5 minutes
+       move_to_ie_timeout: 300000, // 5 minutes
+       move_from_ie_timeout: 300000, // 5 minutes
+       release_ie_timeout: 300000, // 5 minutes
+       slot: null,
+       ie_slot: null,
+       empty_slots: [],
+       full_slots: [],
+       empty_ie_slots: [],
+       full_ie_slots: [],
+       move_to_ie_slots: [],
+       free_ie_slots: [],
+       init: function() {
+               this.set_slots();
+       },
+       set_slots: function() {
+               this.empty_slots = this.get_slots(['slot'], ['E']);
+               this.full_slots = this.get_slots(['slot'], ['F']);
+               this.empty_ie_slots = this.get_slots(['ie_slot'], ['F']);
+               this.full_ie_slots = this.get_slots(['ie_slot'], ['F']);
+       },
+       load_slots_cb: function(data) {
+               oSlotList.data = data;
+               oSlotList.init();
+       },
+       load_drive_window: function(slot) {
+               this.slot = slot;
+               this.prepare_drive_list();
+               this.show_load_drive_window(true);
+       },
+       prepare_drive_list: function() {
+               var sel = document.getElementById(this.ids.load_drive_drives);
+               while(sel.firstChild) {
+                       sel.removeChild(sel.firstChild);
+               }
+               var opt, text, label, state;
+               for (var i = 0; i < oDriveList.data.length; i++) {
+                       opt = document.createElement('OPTION');
+                       opt.value = oDriveList.data[i].drive;
+                       if (oDriveList.data[i].state === 'F') {
+                               opt.setAttribute('disabled', 'disabled');
+                               state = '[<%[ Full ]%>] ';
+                       } else if (oDriveList.data[i].state === 'E') {
+                               state = '[<%[ Empty ]%>] ';
+                       }
+                       text = state + oDriveList.data[i].drive + ' (<%[ index ]%>: ' + oDriveList.data[i].index + ', <%[ device ]%>: ' + oDriveList.data[i].device + ')';
+                       label = document.createTextNode(text);
+                       opt.appendChild(label);
+                       sel.appendChild(opt);
+               }
+       },
+       show_load_drive_window: function(show) {
+               this.set_load_drive_window_slot();
+               var win = document.getElementById(oSlots.ids.load_drive_window);
+               if (show) {
+                       win.style.display = 'block';
+               } else {
+                       win.style.display = 'none';
+               }
+       },
+       set_load_drive_window_slot: function() {
+               document.getElementById(this.ids.load_drive_slot).textContent  = this.slot;
+       },
+       load_drive_loading: function(show) {
+               var loader = document.getElementById(oSlots.ids.load_drive_loading);
+               if (show) {
+                       loader.style.visibility = 'visible';
+               } else {
+                       loader.style.visibility = 'hidden';
+               }
+       },
+       set_drive_window_ok: function() {
+               oSlots.load_drive_loading(false);
+               oSlots.show_load_drive_window(false);
+       },
+       load_drive: function() {
+               var sel = document.getElementById(this.ids.load_drive_drives);
+               var cb = <%=$this->LoadDrive->ActiveControl->Javascript%>;
+               if (!sel.value) {
+                       // no free tape drive available
+                       return false;
+               }
+               var param = {
+                       drive: sel.value,
+                       slot: this.slot
+               };
+               cb.setCallbackParameter(param);
+               cb.setRequestTimeOut(this.load_drive_timeout);
+               cb.dispatch();
+               this.show_changer_loader(true);
+               this.load_drive_loading(true);
+       },
+       set_drive_with_mount_loading_output: function(out_id) {
+               var cb = <%=$this->LoadedDriveWithMount->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(out_id);
+               cb.dispatch();
+       },
+       refresh_drive_with_mount_loading: function(out_id) {
+               setTimeout(function() {
+                       oSlots.set_drive_with_mount_loading_output(out_id);
+               }, 2000)
+       },
+       set_drive_without_mount_loading_output: function(out_id) {
+               var cb = <%=$this->LoadedDriveWithoutMount->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(out_id);
+               cb.dispatch();
+       },
+       refresh_drive_without_mount_loading: function(out_id) {
+               setTimeout(function() {
+                       oSlots.set_drive_without_mount_loading_output(out_id);
+               }, 2000)
+       },
+       get_slots: function(types, states) {
+               if (!states) {
+                       states = ['E', 'F'];
+               }
+               if (!types) {
+                       types = ['slot', 'ie_slot']
+               }
+               var ie_slots = [];
+               for (var i = 0; i < oSlotList.data.length; i++) {
+                       if (types.indexOf(oSlotList.data[i].type) != -1 && states.indexOf(oSlotList.data[i].state) != -1) {
+                               ie_slots.push(oSlotList.data[i].slot_ach);
+                       }
+               }
+               return ie_slots;
+       },
+       set_ie_slots: function() {
+               this.free_ie_slots = oSlots.get_slots(['ie_slot'], ['E']);
+       },
+       show_move_to_ie_window: function(show) {
+               this.set_ie_slots();
+               this.set_move_to_ie_slots();
+               this.move_to_ie_loading(false);
+               var win = document.getElementById(oSlots.ids.move_to_ie_window);
+               if (show) {
+                       win.style.display = 'block';
+               } else {
+                       win.style.display = 'none';
+               }
+       },
+       set_move_to_ie_slots: function() {
+               var slots = [];
+               var selected = oSlotList.table.rows({selected: true}).data();
+               for (var i = 0; i < selected.length; i++) {
+                       if (selected[i].state == 'F') {
+                               slots.push(selected[i].slot_ach);
+                       }
+               }
+               this.move_to_ie_slots = slots;
+               document.getElementById(this.ids.move_to_ie_slots).textContent = slots.join(',');
+       },
+       move_to_ie_loading: function(show) {
+               var loader = document.getElementById(oSlots.ids.move_to_ie_loading);
+               if (show) {
+                       loader.style.visibility = 'visible';
+               } else {
+                       loader.style.visibility = 'hidden';
+               }
+       },
+       move_to_ie: function() {
+               var slot = oSlots.move_to_ie_slots.shift();
+               var ie_slot = oSlots.free_ie_slots.shift();
+               if (slot && ie_slot) {
+                       oSlots.move_to_ie_loading(true);
+                       var param = [slot, ie_slot].join(',');
+                       var cb = <%=$this->MoveToImportExportSlot->ActiveControl->Javascript%>;
+                       cb.setCallbackParameter(param);
+                       cb.RequestTimeOut = oSlots.move_to_ie_timeout;
+                       cb.dispatch();
+                       oSlots.show_changer_loader(true);
+               } else if (!slot) {
+                       oSlots.show_move_to_ie_window(false);
+                       oSlotList.set_release_all_ie_btn();
+               } else if (!ie_slot) {
+                       // There is not enough import/export slots to transfer all selected tapes.
+                       // It shouldn't happen.
+               }
+       },
+       set_move_to_ie_log: function(log) {
+               var logbox = document.getElementById(oSlots.ids.move_to_ie_window_log);
+               if (log && logbox.style.display == 'none') {
+                       logbox.style.display = 'block';
+               }
+               logbox.getElementsByTagName('PRE')[0].textContent = log;
+       },
+       set_move_to_ie_output: function(out_id) {
+               var cb = <%=$this->MovingToImportExportSlot->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(out_id);
+               cb.dispatch();
+       },
+       refresh_move_to_ie_loading: function(out_id) {
+               setTimeout(function() {
+                       oSlots.set_move_to_ie_output(out_id);
+               }, 2000)
+       },
+       show_release_ie_window: function(show) {
+               var win = document.getElementById(oSlots.ids.release_ie_window);
+               if (show) {
+                       oSlots.release_ie_loading(false);
+                       oSlots.set_release_ie_log('');
+                       win.style.display = 'block';
+               } else {
+                       win.style.display = 'none';
+               }
+       },
+       set_release_ie_window_slot: function() {
+               document.getElementById(this.ids.release_ie_slot).textContent  = this.ie_slot;
+       },
+       release_ie_loading: function(show) {
+               var loader = document.getElementById(oSlots.ids.release_ie_loading);
+               if (show) {
+                       loader.style.visibility = 'visible';
+               } else {
+                       loader.style.visibility = 'hidden';
+               }
+       },
+       release_ie_window: function(slot) {
+               this.ie_slot = slot;
+               this.prepare_release_ie();
+               this.set_release_ie_window_slot();
+               this.show_release_ie_window(true);
+       },
+       prepare_release_ie: function() {
+               var sel = document.getElementById(this.ids.release_ie_dest_slot);
+               while(sel.firstChild) {
+                       sel.removeChild(sel.firstChild);
+               }
+               var opt, text, label, state;
+               for (var i = 0; i < oSlotList.data.length; i++) {
+                       opt = document.createElement('OPTION');
+                       opt.value = oSlotList.data[i].slot_ach;
+                       if (oSlotList.data[i].state === 'F') {
+                               opt.setAttribute('disabled', 'disabled');
+                               state = '[<%[ Full ]%>] ';
+                       } else if (oSlotList.data[i].state === 'E') {
+                               state = '[<%[ Empty ]%>] ';
+                       }
+                       text = state + '<%[ Slot ]%>: ' + oSlotList.data[i].slot_ach;
+                       label = document.createTextNode(text);
+                       opt.appendChild(label);
+                       sel.appendChild(opt);
+               }
+       },
+       release_ie: function(finish) {
+               var sel = document.getElementById(oSlots.ids.release_ie_dest_slot);
+               if (oSlots.ie_slot && sel.value && !finish) {
+                       oSlots.release_ie_loading(true);
+                       var param = [oSlots.ie_slot, sel.value].join(',');
+                       var cb = <%=$this->ReleaseImportExportSlot->ActiveControl->Javascript%>;
+                       cb.setCallbackParameter(param);
+                       cb.RequestTimeOut = oSlots.release_ie_timeout;
+                       cb.dispatch();
+                       oSlots.show_changer_loader(true);
+               } else if (finish) {
+                       oSlots.show_release_ie_window(false);
+                       oSlotList.set_release_all_ie_btn();
+               }
+       },
+       set_release_ie_log: function(log) {
+               var logbox = document.getElementById(oSlots.ids.release_ie_window_log);
+               if (log && logbox.style.display == 'none') {
+                       logbox.style.display = 'block';
+               }
+               logbox.getElementsByTagName('PRE')[0].textContent = log;
+       },
+       set_release_ie_output: function(out_id) {
+               var cb = <%=$this->ReleasingImportExportSlot->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(out_id);
+               cb.dispatch();
+       },
+       refresh_release_ie_loading: function(out_id) {
+               setTimeout(function() {
+                       oSlots.set_release_ie_output(out_id);
+               }, 2000)
+       },
+       release_all_ie: function() {
+               var fie_len = oSlots.full_ie_slots.length;
+               var es_len = oSlots.empty_slots.length;
+               if  (fie_len > es_len) {
+                       var emsg = '<%[ There are to few regular slots to transfer selected volumes from import/export slots. Full import/export slot count: %ie_slot_count, free regular slot count: %slot_count. ]%>';
+                       emsg = emsg.replace('%ie_slot_count', fie_len);
+                       emsg = emsg.replace('%slot_count', es_len);
+                       oBulkActionsModal.set_error(emsg);
+                       return false;
+               }
+               var ie_slot = oSlots.full_ie_slots.shift()
+               var slot = oSlots.empty_slots.shift();
+
+               if (slot && ie_slot) {
+                       var param = [ie_slot, slot].join(',');
+                       var cb = <%=$this->MoveFromImportExportSlot->ActiveControl->Javascript%>;
+                       cb.setCallbackParameter(param);
+                       cb.RequestTimeOut = oSlots.move_from_ie_timeout;
+                       cb.dispatch();
+                       oSlots.show_changer_loader(true);
+               } else if (!slot) {
+                       oSlotList.set_release_all_ie_btn();
+               }
+       },
+       set_release_all_ie_output: function(out_id) {
+               var cb = <%=$this->MovingFromImportExportSlot->ActiveControl->Javascript%>;
+               cb.setCallbackParameter(out_id);
+               cb.dispatch();
+       },
+       refresh_release_all_ie_loading: function(out_id) {
+               setTimeout(function() {
+                       oSlots.set_release_all_ie_output(out_id);
+               }, 2000)
+       },
+       show_changer_loader: function(show) {
+               var loader = document.getElementById(oSlots.ids.changer_loader);
+               if (show) {
+                       loader.style.display = '';
+               } else {
+                       loader.style.display = 'none';
+               }
+       }
+};
+               </script>
+               <com:TCallback ID="LoadDrive" OnCallback="loadDrive" />
+               <com:TCallback ID="LoadedDriveWithoutMount" OnCallback="loadedDriveWithoutMount" />
+               <com:TCallback ID="LoadedDriveWithMount" OnCallback="loadedDriveWithMount" />
+               <div id="load_drive_window" class="w3-modal">
+                       <div class="w3-modal-content w3-animate-top w3-card-4">
+                               <header class="w3-container w3-teal">
+                                       <span onclick="oSlots.show_load_drive_window(false);" class="w3-button w3-display-topright">&times;</span>
+                                       <h2><%[ Load drive ]%></h2>
+                               </header>
+                               <div class="w3-container w3-margin-left w3-margin-right w3-margin-top w3-text-teal">
+                                       <div class="w3-row w3-section">
+                                               <div class="w3-col w3-third">
+                                                       <%[ Slot ]%>:
+                                               </div>
+                                               <div class="w3-col w3-half">
+                                                       <strong id="load_drive_slot"></strong>
+                                               </div>
+                                       </div>
+                                       <div class="w3-row w3-section">
+                                               <div class="w3-col w3-third">
+                                                       <label for="load_drive_drives"><%[ Drive ]%>:</label>
+                                               </div>
+                                               <div class="w3-col w3-half">
+                                                       <select id="load_drive_drives" class="w3-select w3-border"></select>
+                                               </div>
+                                       </div>
+                                       <div class="w3-row w3-section">
+                                               <div class="w3-col w3-third">
+                                                       <label for="load_drive_mount"><%[ Mount volume ]%>:</label>
+                                               </div>
+                                               <div class="w3-col w3-half">
+                                                       <com:TActiveCheckBox ID="LoadDriveMount" CssClass="w3-check" Checked="true" />
+                                               </div>
+                                       </div>
+                               </div>
+                               <footer class="w3-container w3-center w3-padding">
+                                       <button type="button" class="w3-button w3-red" onclick="oSlots.show_load_drive_window(false);"><i class="fas fa-times"></i> &nbsp;<%[ Cancel ]%></button>
+                                       <button type="button" class="w3-button w3-green w3-show-inline-block" onclick="oSlots.load_drive();"><i class="fas fa-download"></i> &nbsp;<%[ Load drive ]%></button>
+                                       <i id="load_drive_loading" class="fas fa-sync-alt fa-spin" style="visibility: hidden"></i>
+                               </footer>
+                       </div>
+               </div>
+               <div id="move_to_ie_window" class="w3-modal">
+                       <div class="w3-modal-content w3-animate-top w3-card-4">
+                               <header class="w3-container w3-teal">
+                                       <span onclick="oSlots.show_move_to_ie_window(false);" class="w3-button w3-display-topright">&times;</span>
+                                       <h2><%[ Move to import/export slot ]%></h2>
+                               </header>
+                               <div class="w3-container w3-margin-left w3-margin-right w3-margin-top w3-text-teal">
+                                       <div class="w3-row w3-section">
+                                               <div class="w3-col w3-third">
+                                                       <%[ Slots ]%>:
+                                               </div>
+                                               <div class="w3-col w3-half">
+                                                       <strong id="move_to_ie_slots"></strong>
+                                               </div>
+                                       </div>
+                               </div>
+                               <footer class="w3-container w3-center w3-padding">
+                                       <button type="button" class="w3-button w3-red" onclick="oSlots.show_move_to_ie_window(false);"><i class="fas fa-times"></i> &nbsp;<%[ Cancel ]%></button>
+                                       <button type="button" class="w3-button w3-green w3-show-inline-block" onclick="oSlots.move_to_ie();"><i class="fas fa-download"></i> &nbsp;<%[ Move to import/export slots ]%></button>
+                                       <i id="move_to_ie_loading" class="fas fa-sync-alt fa-spin" style="visibility: hidden"></i>
+                               </footer>
+                               <div class="w3-margin-left w3-margin-right" style="max-height: 400px; overflow-x: auto;">
+                                       <div id="move_to_ie_window_log" class="w3-code" style="display: none">
+                                               <pre></pre>
+                                       </div>
+                               </div>
+                       </div>
+               </div>
+               <div id="release_ie_window" class="w3-modal">
+                       <div class="w3-modal-content w3-animate-top w3-card-4">
+                               <header class="w3-container w3-teal">
+                                       <span onclick="oSlots.show_release_ie_window(false);" class="w3-button w3-display-topright">&times;</span>
+                                       <h2><%[ Release import/export slot ]%></h2>
+                               </header>
+                               <div class="w3-container w3-margin-left w3-margin-right w3-margin-top w3-text-teal">
+                                       <div class="w3-row w3-section">
+                                               <div class="w3-col w3-third">
+                                                       <%[ Import/export slot ]%>:
+                                               </div>
+                                               <div class="w3-col w3-half">
+                                                       <strong id="release_ie_slot"></strong>
+                                               </div>
+                                       </div>
+                                       <div class="w3-row w3-section">
+                                               <div class="w3-col w3-third">
+                                                       <label for="release_ie_dest_slot"><%[ Destination slot ]%>:</label>
+                                               </div>
+                                               <div class="w3-col w3-half">
+                                                       <select id="release_ie_dest_slot" class="w3-select w3-border"></select>
+                                               </div>
+                                       </div>
+                               </div>
+                               <footer class="w3-container w3-center w3-padding">
+                                       <button type="button" class="w3-button w3-red" onclick="oSlots.show_release_ie_window(false);"><i class="fas fa-times"></i> &nbsp;<%[ Cancel ]%></button>
+                                       <button type="button" class="w3-button w3-green w3-show-inline-block" onclick="oSlots.release_ie();"><i class="fas fa-download"></i> &nbsp;<%[ Release I/E ]%></button>
+                                       <i id="release_ie_loading" class="fas fa-sync-alt fa-spin" style="visibility: hidden"></i>
+                               </footer>
+                               <div class="w3-margin-left w3-margin-right" style="max-height: 400px; overflow-x: auto;">
+                                       <div id="release_ie_window_log" class="w3-code" style="display: none">
+                                               <pre></pre>
+                                       </div>
+                               </div>
+                       </div>
+               </div>
+       </div>
+       <com:Application.Web.Portlets.BulkActionsModal ID="BulkActions" />
 </com:TContent>
index 084eed46fa3f33f3e06c133b76496f5953867cce..cc3a96b894c46a19c8cc73ca6aace9843c49a0a1 100644 (file)
@@ -26,6 +26,7 @@ Prado::using('System.Web.UI.ActiveControls.TActiveTextBox');
 Prado::using('System.Web.UI.ActiveControls.TActiveRepeater');
 Prado::using('System.Web.UI.ActiveControls.TActiveLinkButton');
 Prado::using('Application.Common.Class.Params');
+Prado::using('Application.Common.Class.Errors');
 Prado::using('Application.Web.Class.BaculumWebPage'); 
 
 /**
@@ -77,7 +78,6 @@ class StorageView extends BaculumWebPage {
                $storageshow = $this->Application->getModule('api')->get(
                        array('storages', $storage->storageid, 'show')
                )->output;
-               $this->StorageActionLog->Text = implode(PHP_EOL, $storageshow);
                $this->setStorageDevice($storageshow);
                $this->setDevices();
        }
@@ -257,57 +257,211 @@ class StorageView extends BaculumWebPage {
                $this->getCallbackClient()->callClientFunction('init_graphical_storage_status', [$storage_status]);
        }
 
+       private function actionLoading($result, $out_id, $refresh_func) {
+               $messages_log = $this->getModule('messages_log');
+               if ($result->error === 0) {
+                       $rlen = count($result->output);
+                       $last = $rlen > 0 ? trim($result->output[$rlen-1]) : '';
+                       if ($last === 'quit') {
+                               array_pop($result->output);
+                       }
+                       $messages_log->append($result->output);
+                       if (count($result->output) > 0) {
+                               // log messages
+                               $output = implode('', $result->output);
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oStorageActions.log',
+                                       [$output]
+                               );
+                               // refresh output periodically
+                               $this->getCallbackClient()->callClientFunction(
+                                       $refresh_func,
+                                       [$out_id]
+                               );
+                       } else {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oStorageActions.show_loader',
+                                       [false]
+                               );
+                       }
+               } else {
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $messages_log->append($result->output);
+                       $this->getCallbackClient()->callClientFunction(
+                               'oStorageActions.log',
+                               [$emsg]
+                       );
+               }
+       }
+
+       private function logActionError($result) {
+               $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+               $messages_log->append($result->output);
+               $this->getCallbackClient()->callClientFunction(
+                       'oStorageActions.log',
+                       [$emsg]
+               );
+       }
+
        public function mount($sender, $param) {
                $drive = $this->getIsAutochanger() ? intval($this->Drive->Text) : 0;
                $slot = $this->getIsAutochanger() ? intval($this->Slot->Text) : 0;
+               $result = $this->mountAction($drive, $slot);
+               if ($result->error === 0 && count($result->output) == 1) {
+                       $out = json_decode($result->output[0]);
+                       if (is_object($out) && property_exists($out, 'out_id')) {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oStorageActions.refresh_mount',
+                                       [$out->out_id]
+                               );
+                       }
+               } else {
+               }
+       }
+
+       private function mountAction($drive, $slot) {
                $params = [
                        'drive' => $drive,
                        'slot' => $slot
                ];
                $query = '?' . http_build_query($params);
-               $mount = $this->getModule('api')->get(
-                       array('storages', $this->getStorageId(), 'mount', $query)
+               $result = $this->getModule('api')->set(
+                       [
+                               'storages',
+                               $this->getStorageId(),
+                               'mount',
+                               $query
+                       ]
+               );
+               return $result;
+       }
+
+       public function mountLoading($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'storages',
+                       $this->getStorageId(),
+                       'mount',
+                       $query
+               ]);
+               $this->actionLoading(
+                       $result,
+                       $out_id,
+                       'oStorageActions.refresh_mount'
                );
-               if ($mount->error === 0) {
-                       $this->StorageActionLog->Text = implode(PHP_EOL, $mount->output);
-               } else {
-                       $this->StorageActionLog->Text = $mount->output;
-               }
        }
 
        public function umount($sender, $param) {
                $drive = $this->getIsAutochanger() ? intval($this->Drive->Text) : 0;
+               $result = $this->umountAction($drive);
+               if ($result->error === 0) {
+                       if (count($result->output) == 1) {
+                               $out = json_decode($result->output[0]);
+                               if (is_object($out) && property_exists($out, 'out_id')) {
+                                       $this->getCallbackClient()->callClientFunction(
+                                               'oStorageActions.refresh_umount',
+                                               [$out->out_id]
+                                       );
+                               }
+                       }
+               } else {
+                       $this->logActionError($result);
+               }
+       }
+
+       private function umountAction($drive) {
                $params = [
                        'drive' => $drive
                ];
                $query = '?' . http_build_query($params);
-               $umount = $this->getModule('api')->get(
-                       array('storages', $this->getStorageId(), 'umount', $query)
+               $result = $this->getModule('api')->set(
+                       [
+                               'storages',
+                               $this->getStorageId(),
+                               'umount',
+                               $query
+                       ]
                );
-               if ($umount->error === 0) {
-                       $this->StorageActionLog->Text = implode(PHP_EOL, $umount->output);
-               } else {
-                       $this->StorageActionLog->Text = $umount->output;
-               }
+               return $result;
+       }
 
+       public function umountLoading($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'storages',
+                       $this->getStorageId(),
+                       'umount',
+                       $query
+               ]);
+               $this->actionLoading(
+                       $result,
+                       $out_id,
+                       'oStorageActions.refresh_umount'
+               );
        }
 
        public function release($sender, $param) {
                $drive = $this->getIsAutochanger() ? intval($this->Drive->Text) : 0;
+               $result = $this->releaseAction($drive);
+               if ($result->error === 0) {
+                       if (count($result->output) == 1) {
+                               $out = json_decode($result->output[0]);
+                               if (is_object($out) && property_exists($out, 'out_id')) {
+                                       $this->getCallbackClient()->callClientFunction(
+                                               'oStorageActions.refresh_release',
+                                               [$out->out_id]
+                                       );
+                               }
+                       }
+               } else {
+                       $this->logActionError($result);
+               }
+       }
+
+       private function releaseAction($drive) {
                $params = [
                        'drive' => $drive
                ];
                $query = '?' . http_build_query($params);
-               $release = $this->getModule('api')->get(
-                       array('storages', $this->getStorageId(), 'release', $query)
+               $result = $this->getModule('api')->set(
+                       [
+                               'storages',
+                               $this->getStorageId(),
+                               'release',
+                               $query
+                       ]
                );
-               if ($release->error === 0) {
-                       $this->StorageActionLog->Text = implode(PHP_EOL, $release->output);
-               } else {
-                       $this->StorageActionLog->Text = $release->output;
-               }
+               return $result;
        }
 
+       public function releaseLoading($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'storages',
+                       $this->getStorageId(),
+                       'release',
+                       $query
+               ]);
+               $this->actionLoading(
+                       $result,
+                       $out_id,
+                       'oStorageActions.refresh_release'
+               );
+       }
+
+
        public function setAutochanger($sender, $param) {
                if ($this->getIsAutochanger()) {
                        $this->AutochangerConfig->setComponentName($_SESSION['sd']);
@@ -323,5 +477,512 @@ class StorageView extends BaculumWebPage {
                $this->StorageConfig->setLoadValues(true);
                $this->StorageConfig->raiseEvent('OnDirectiveListLoad', $this, null);
        }
+
+       public function loadAutochanger($sender, $param) {
+               $result = $this->getModule('api')->get([
+                       'devices',
+                       $this->getDeviceName(),
+                       'listall'
+               ]);
+               $cb = $this->getCallbackClient();
+               if ($result->error === 0) {
+                       $cb->show('drive_list_container');
+                       $cb->show('slot_list_container');
+                       $cb->hide('manage_autochanger_not_available');
+                       $cb->callClientFunction(
+                               'oDrives.load_drives_cb',
+                               [$result->output->drives]
+                       );
+                       $slots = array_merge(
+                               $result->output->slots,
+                               $result->output->ie_slots
+                       );
+                       $cb->callClientFunction(
+                               'oSlots.load_slots_cb',
+                               [$slots]
+                       );
+               } elseif ($result->error === DeviceError::ERROR_DEVICE_DEVICE_CONFIG_DOES_NOT_EXIST || $result->error === DeviceError::ERROR_DEVICE_AUTOCHANGER_DOES_NOT_EXIST) {
+                       $cb->hide('drive_list_container');
+                       $cb->hide('slot_list_container');
+                       $cb->show('manage_autochanger_not_available');
+               }
+       }
+
+       public function loadDrive($sender, $param) {
+               $data = $param->getCallbackParameter();
+               if (!is_object($data)) {
+                       return;
+               }
+               $result = [];
+               if ($this->LoadDriveMount->Checked) {
+                       $parameters = [
+                               'device' => $data->drive,
+                               'slot' => $data->slot
+                       ];
+                       $query = '?' . http_build_query($parameters);
+                       $result = $this->getModule('api')->set([
+                               'storages',
+                               $this->getStorageId(),
+                               'mount',
+                               $query
+                       ]);
+               } else {
+                       $parameters = [
+                               'drive' => $data->drive,
+                               'slot' => $data->slot
+                       ];
+                       $query = '?' . http_build_query($parameters);
+                       $result = $this->getModule('api')->set([
+                               'devices',
+                               $this->getDeviceName(),
+                               'load',
+                               $query
+                       ]);
+               }
+               if ($result->error === 0) {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.set_drive_window_ok'
+                       );
+                       $out_id = '';
+                       if ($this->LoadDriveMount->Checked) {
+                               if (count($result->output) == 1) {
+                                       $out = json_decode($result->output[0]);
+                                       if (is_object($out) && property_exists($out, 'out_id')) {
+                                               $out_id = $out->out_id;
+                                       }
+                               }
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.refresh_drive_with_mount_loading',
+                                       [$out_id]
+                               );
+                       } else {
+                               $out_id = $result->output->out_id;
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.refresh_drive_without_mount_loading',
+                                       [$out_id]
+                               );
+                       }
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $this->getModule('messages_log')->append([$emsg]);
+               }
+       }
+
+       public function loadedDriveWithMount($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'storages',
+                       $this->getStorageId(),
+                       'mount',
+                       $query
+               ]);
+               $this->loadedDrive(
+                       'oSlots.refresh_drive_with_mount_loading',
+                       $out_id,
+                       $result
+               );
+       }
+
+       public function loadedDriveWithoutMount($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'devices',
+                       $this->getDeviceName(),
+                       'load',
+                       $query
+               ]);
+               $this->loadedDrive(
+                       'oSlots.refresh_drive_without_mount_loading',
+                       $out_id,
+                       $result
+               );
+       }
+
+       public function loadedDrive($refresh_func, $out_id, $result) {
+               $messages_log = $this->getModule('messages_log');
+               if ($result->error === 0) {
+                       $rlen = count($result->output);
+                       $last = $rlen > 0 ? trim($result->output[$rlen-1]) : '';
+                       if ($last === 'quit') {
+                               array_pop($result->output);
+                       }
+                       $messages_log->append($result->output);
+               } else {
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $messages_log->append($result->output);
+               }
+               if ($result->error === 0) {
+                       if (count($result->output) > 0) {
+                               // refresh output periodically
+                               $this->getCallbackClient()->callClientFunction(
+                                       $refresh_func,
+                                       [$out_id]
+                               );
+                       } else {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.show_changer_loader',
+                                       [false]
+                               );
+                               // finish refreshing output
+                               $this->loadAutochanger(null, null);
+                       }
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $this->getModule('messages_log')->append([$emsg]);
+               }
+       }
+
+       public function unloadDrive($sender, $param) {
+               $data = $param->getCallbackParameter();
+               if (!is_object($data)) {
+                       return;
+               }
+               $parameters = [
+                       'device' => $data->drive,
+                       'slot' => $data->slot
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->set([
+                       'storages',
+                       $this->getStorageId(),
+                       'release',
+                       $query
+               ]);
+               if ($result->error === 0) {
+                       $out_id = '';
+                       if (count($result->output) == 1) {
+                               $out = json_decode($result->output[0]);
+                               if (is_object($out) && property_exists($out, 'out_id')) {
+                                       $out_id = $out->out_id;
+                               }
+                       }
+                       $this->getCallbackClient()->callClientFunction(
+                               'oDrives.refresh_drive_unloading',
+                               [$out_id]
+                       );
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $this->getModule('messages_log')->append([$emsg]);
+               }
+       }
+
+       public function unloadedDrive($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'storages',
+                       $this->getStorageId(),
+                       'release',
+                       $query
+               ]);
+               $messages_log = $this->getModule('messages_log');
+               if ($result->error === 0) {
+                       $rlen = count($result->output);
+                       $last = $rlen > 0 ? trim($result->output[$rlen-1]) : '';
+                       if ($last === 'quit') {
+                               array_pop($result->output);
+                       }
+                       $messages_log->append($result->output);
+                       if (count($result->output) > 0) {
+                               // refresh output periodically
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oDrives.refresh_drive_unloading',
+                                       [$out_id]
+                               );
+                       } else {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.show_changer_loader',
+                                       [false]
+                               );
+                               // finish refreshing output
+                               $this->loadAutochanger(null, null);
+                       }
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $messages_log->append($result->output);
+               }
+       }
+
+       public function labelBarcodes($sender, $param) {
+               $slots_ach = explode('|', $param->getCallbackParameter());
+               $this->LabelBarcodes->setSlots($slots_ach);
+               $this->LabelBarcodes->loadValues();
+       }
+
+       public function labelComplete($sender, $param) {
+               $this->getCallbackClient()->callClientFunction(
+                       'show_label_volume_window',
+                       [false]
+               );
+               $this->getCallbackClient()->callClientFunction(
+                       'oSlots.show_changer_loader',
+                       [false]
+               );
+               $this->loadAutochanger(null, null);
+       }
+
+       private function transferSlots($slotsrc, $slotdest) {
+               $parameters = [
+                       'slotsrc' => $slotsrc,
+                       'slotdest' => $slotdest
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->set([
+                       'devices',
+                       $this->getDeviceName(),
+                       'transfer',
+                       $query
+               ]);
+               return $result;
+       }
+
+       private function getTransferOutput($out_id) {
+               $parameters = [
+                       'out_id' => $out_id
+               ];
+               $query = '?' . http_build_query($parameters);
+               $result = $this->getModule('api')->get([
+                       'devices',
+                       $this->getDeviceName(),
+                       'transfer',
+                       $query
+               ]);
+               return $result;
+       }
+
+       public function moveToIE($sender, $param) {
+               list($slot_ach, $ie_slot) = explode(',', $param->getCallbackParameter(), 2);
+               $result = $this->transferSlots($slot_ach, $ie_slot);
+               if ($result->error === 0) {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.refresh_move_to_ie_loading',
+                               [$result->output->out_id]
+                       );
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $this->getModule('messages_log')->append([$emsg]);
+               }
+       }
+
+       public function movingToIE($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $result = $this->getTransferOutput($out_id);
+               $messages_log = $this->getModule('messages_log');
+               if ($result->error === 0) {
+                       if (count($result->output) > 0) {
+                               // refresh output periodically
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.refresh_move_to_ie_loading',
+                                       [$out_id]
+                               );
+                               $rlen = count($result->output);
+                               $last = $rlen > 0 ? trim($result->output[$rlen-1]) : '';
+                               if ($last === 'quit') {
+                                       array_pop($result->output);
+                               }
+                               $messages_log->append($result->output);
+                               $out = implode(PHP_EOL, $result->output);
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.set_move_to_ie_log',
+                                       [$out]
+                               );
+                       } else {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.show_changer_loader',
+                                       [false]
+                               );
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.move_to_ie'
+                               );
+                               // finish refreshing output
+                               $this->loadAutochanger(null, null);
+                       }
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $messages_log->append($result->output);
+               }
+       }
+
+       public function releaseIE($sender, $param) {
+               list($ie_slot, $slot_ach) = explode(',', $param->getCallbackParameter(), 2);
+               $result = $this->transferSlots($ie_slot, $slot_ach);
+               if ($result->error === 0) {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.refresh_release_ie_loading',
+                               [$result->output->out_id]
+                       );
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $this->getModule('messages_log')->append([$emsg]);
+               }
+       }
+
+       public function releasingIE($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $result = $this->getTransferOutput($out_id);
+               $messages_log = $this->getModule('messages_log');
+               if ($result->error === 0) {
+                       if (count($result->output) > 0) {
+                               // refresh output periodically
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.refresh_release_ie_loading',
+                                       [$out_id]
+                               );
+                               $rlen = count($result->output);
+                               $last = $rlen > 0 ? trim($result->output[$rlen-1]) : '';
+                               if ($last === 'quit') {
+                                       array_pop($result->output);
+                               }
+                               $messages_log->append($result->output);
+                               $out = implode(PHP_EOL, $result->output);
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.set_release_ie_log',
+                                       [$out]
+                               );
+                       } else {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.show_changer_loader',
+                                       [false]
+                               );
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.release_ie',
+                                       [true]
+                               );
+
+                               // finish refreshing output
+                               $this->loadAutochanger(null, null);
+                       }
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $messages_log->append($result->output);
+               }
+       }
+
+       public function updateSlotsBarcodes($sender, $param) {
+               $slots_ach = explode('|', $param->getCallbackParameter());
+               $this->UpdateSlots->BarcodeUpdate = true;
+               $this->UpdateSlots->setSlots($slots_ach);
+               $this->UpdateSlots->loadValues();
+       }
+
+       public function updateSlots($sender, $param) {
+               $slots_ach = explode('|', $param->getCallbackParameter());
+               $this->UpdateSlots->BarcodeUpdate = false;
+               $this->UpdateSlots->setSlots($slots_ach);
+               $this->UpdateSlots->loadValues();
+       }
+
+       public function moveFromIE($sender, $param) {
+               list($ie_slot, $slot_ach) = explode(',', $param->getCallbackParameter(), 2);
+               $result = $this->transferSlots($ie_slot, $slot_ach);
+               if ($result->error === 0) {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.refresh_release_all_ie_loading',
+                               [$result->output->out_id]
+                       );
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $this->getModule('messages_log')->append([$emsg]);
+               }
+       }
+
+       public function movingFromIE($sender, $param) {
+               $out_id = $param->getCallbackParameter();
+               $result = $this->getTransferOutput($out_id);
+               $messages_log = $this->getModule('messages_log');
+               if ($result->error === 0) {
+                       if (count($result->output) > 0) {
+                               // refresh output periodically
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.refresh_release_all_ie_loading',
+                                       [$out_id]
+                               );
+                               $rlen = count($result->output);
+                               $last = $rlen > 0 ? trim($result->output[$rlen-1]) : '';
+                               if ($last === 'quit') {
+                                       array_pop($result->output);
+                               }
+                               $messages_log->append($result->output);
+                       } else {
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.show_changer_loader',
+                                       [false]
+                               );
+                               $this->getCallbackClient()->callClientFunction(
+                                       'oSlots.release_all_ie'
+                               );
+                               // finish refreshing output
+                               $this->loadAutochanger(null, null);
+                       }
+               } else {
+                       $this->getCallbackClient()->callClientFunction(
+                               'oSlots.show_changer_loader',
+                               [false]
+                       );
+                       $emsg = sprintf('Error %s: %s', $result->error, $result->output);
+                       $messages_log->append($result->output);
+               }
+       }
+
+       public function showChangerLoading($sender, $param) {
+               $this->getCallbackClient()->callClientFunction(
+                       'oSlots.show_changer_loader',
+                       [true]
+               );
+       }
+
+       public function hideChangerLoading($sender, $param) {
+               $this->getCallbackClient()->callClientFunction(
+                       'oSlots.show_changer_loader',
+                       [false]
+               );
+       }
 }
 ?>
index 21c7e98fb52efa7187825f4b18e9c7828235459d..27f3d3320eee2a0330e304dc7c94ed44fbf1d1c9 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2021 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -36,6 +36,10 @@ Prado::using('Application.Web.Portlets.Portlets');
  */
 class LabelVolume extends Portlets {
 
+       const SHOW_BUTTON = 'ShowButton';
+       const BARCODE_LABEL = 'BarcodeLabel';
+       const STORAGE = 'Storage';
+
        public function loadValues() {
                $storages = $this->getModule('api')->get(array('storages'));
                $storage_list = array();
@@ -44,7 +48,11 @@ class LabelVolume extends Portlets {
                                $storage_list[$storage->storageid] = $storage->name;
                        }
                }
-               $this->StorageLabel->dataSource = $storage_list;
+               $this->StorageLabel->DataSource = $storage_list;
+               if ($this->Storage) {
+                       $storage_list_flip =array_flip($storage_list);
+                       $this->StorageLabel->SelectedValue = $storage_list_flip[$this->Storage];
+               }
                $this->StorageLabel->dataBind();
 
                $pools = $this->Application->getModule('api')->get(array('pools'));
@@ -60,7 +68,7 @@ class LabelVolume extends Portlets {
 
        public function labelVolumes($sender, $param) {
                $result = null;
-               if ($this->Barcodes->Checked == true) {
+               if ($this->Barcodes->Checked || $this->BarcodeLabel) {
                        $params = array(
                                'slots' => $this->SlotsLabel->Text,
                                'drive' => $this->DriveLabel->Text,
@@ -95,8 +103,10 @@ class LabelVolume extends Portlets {
                if ($result->error === 0) {
                        $this->getPage()->getCallbackClient()->callClientFunction('set_labeling_status', array('loading'));
                        $this->LabelVolumeLog->Text = implode('', $result->output);
+                       $this->onLabelStart($param);
                } else {
                        $this->LabelVolumeLog->Text = $result->output;
+                       $this->onLabelFail($param);
                }
        }
 
@@ -129,10 +139,60 @@ class LabelVolume extends Portlets {
                                $this->getPage()->getCallbackClient()->callClientFunction('label_volume_output_refresh', array($out_id));
                        } else {
                                $this->getPage()->getCallbackClient()->callClientFunction('set_labeling_status', array('finish'));
+                               $this->onLabelSuccess($param);
+                               $this->onLabelComplete($param);
                        }
                } else {
                        $this->LabelVolumeLog->Text = $result->output;
+                       $this->onLabelFail($param);
+                       $this->onLabelComplete($param);
                }
        }
+
+       public function onLabelStart($param) {
+               $this->raiseEvent('OnLabelStart', $this, $param);
+       }
+
+       public function onLabelComplete($param) {
+               $this->raiseEvent('OnLabelComplete', $this, $param);
+       }
+
+       public function onLabelSuccess($param) {
+               $this->raiseEvent('OnLabelSuccess', $this, $param);
+       }
+
+       public function onLabelFail($param) {
+               $this->raiseEvent('OnLabelFail', $this, $param);
+       }
+
+       public function setSlots(array $slots) {
+               $this->SlotsLabel->Text = implode(',', $slots);
+       }
+
+       public function setShowButton($show) {
+               $show = TPropertyValue::ensureBoolean($show);
+               $this->setViewState(self::SHOW_BUTTON, $show);
+       }
+
+       public function getShowButton() {
+               return $this->getViewState(self::SHOW_BUTTON, true);
+       }
+
+       public function setBarcodeLabel($barcode_label) {
+               $barcode_label = TPropertyValue::ensureBoolean($barcode_label);
+               $this->setViewState(self::BARCODE_LABEL, $barcode_label);
+       }
+
+       public function getBarcodeLabel() {
+               return $this->getViewState(self::BARCODE_LABEL);
+       }
+
+       public function setStorage($storage) {
+               $this->setViewState(self::STORAGE, $storage);
+       }
+
+       public function getStorage() {
+               return $this->getViewState(self::STORAGE);
+       }
 }
 ?>
index 0c5e78d29a486e6c0cc675c30a39310fc80f0511..c8ecd2ff9c4c5db7fda2118bbcd3e1331969cce0 100644 (file)
@@ -1,14 +1,10 @@
 <com:TActiveLinkButton
        CssClass="w3-button w3-green"
        OnClick="loadValues"
+       Visible="<%=$this->ShowButton%>"
 >
        <prop:Attributes.onclick>
-               var logbox = document.getElementById('<%=$this->LabelVolumeLog->ClientID%>');
-               logbox.innerHTML = '';
-               var logbox_container = document.getElementById('label_volume_log');
-               logbox_container.style.display = 'none';
-               set_labeling_status('start');
-               document.getElementById('label_volume').style.display = 'block';
+               show_label_volume_window(true);
        </prop:Attributes.onclick>
        <i class="fa fa-tag"></i> &nbsp;<%[ Label volume(s) ]%>
 </com:TActiveLinkButton>
@@ -16,7 +12,7 @@
 <div id="label_volume" class="w3-modal">
        <div class="w3-modal-content w3-animate-top w3-card-4">
                <header class="w3-container w3-teal"> 
-                       <span onclick="document.getElementById('label_volume').style.display='none'" class="w3-button w3-display-topright">&times;</span>
+                       <span onclick="show_label_volume_window(false);" class="w3-button w3-display-topright">&times;</span>
                        <h2><%[ Label volume(s) ]%></h2>
                </header>
                <div class="w3-padding">
@@ -56,7 +52,7 @@
                        />
                        <div class="w3-row-padding w3-section-padding w3-section">
                                <div class="w3-col w3-half"><com:TLabel ForControl="Barcodes" Text="<%[ Use barcodes as label: ]%>" /></div>
-                               <div class="w3-col w3-half"><com:TActiveCheckBox ID="Barcodes" CssClass="w3-check" Attributes.onclick="set_barcodes();"/></div>
+                               <div class="w3-col w3-half"><com:TActiveCheckBox ID="Barcodes" CssClass="w3-check" Attributes.onclick="set_label_volume_barcodes();"/></div>
                        </div>
                        <div id="label_with_name" class="w3-row-padding w3-section">
                                <div class="w3-col w3-half"><com:TLabel ForControl="LabelName" Text="<%[ Label name: ]%>" /></div>
                                <div class="w3-col w3-half"><com:TLabel ForControl="PoolLabel" Text="<%[ Pool: ]%>" /></div>
                                <div class="w3-col w3-half"><com:TActiveDropDownList ID="PoolLabel" CssClass="w3-select w3-border" /></div>
                        </div>
-                       <div class="w3-row-padding w3-section">
+                       <div class="w3-row-padding w3-section"<%=$this->Storage ? ' style="display: none"' : ''%>>
                                <div class="w3-col w3-half"><com:TLabel ForControl="StorageLabel" Text="<%[ Storage: ]%>" /></div>
                                <div class="w3-col w3-half"><com:TActiveDropDownList ID="StorageLabel" CssClass="w3-select w3-border" /></div>
                        </div>
                        <div class="w3-row-padding w3-section">
-                               <div class="w3-col w3-half"><com:TLabel ForControl="DriveLabel" Text="<%[ Drive number: ]%>" /></div>
+                               <div class="w3-col w3-half"><com:TLabel ForControl="DriveLabel" Text="<%[ Drive index: ]%>" /></div>
                                <div class="w3-col w3-half">
                                        <com:TActiveTextBox ID="DriveLabel" CssClass="w3-input w3-border" Text="0" />
                                        <com:TRequiredFieldValidator
                                </div>
                        </div>
                        <div class="w3-container w3-center w3-section">
-                               <button type="button" class="w3-button w3-red" onclick="document.getElementById('label_volume').style.display='none';"><i class="fa fa-times"></i> &nbsp;<%[ Close ]%></button>
+                               <button type="button" class="w3-button w3-red" onclick="show_label_volume_window(false);"><i class="fa fa-times"></i> &nbsp;<%[ Close ]%></button>
                                <com:TActiveLinkButton
                                        ID="LabelButton"
                                        CausesValidation="true"
 </com:TCallback>
 <script type="text/javascript">
 var label_volume_logbox_scroll = false;
-function set_barcodes() {
+function set_label_volume_barcodes(force_barcodes) {
        var chkb = document.getElementById('<%=$this->Barcodes->ClientID%>');
+       if (force_barcodes) {
+               chkb.checked = true;
+               chkb.setAttribute('disabled', 'disabled');
+       }
        var name_el = document.getElementById('label_with_name');
        var with_barcodes_el = document.getElementById('label_with_barcodes');
        var without_barcodes_el = document.getElementById('label_without_barcodes');
@@ -230,4 +230,15 @@ function set_label_volume_output(out_id) {
        cb.setCallbackParameter(out_id);
        cb.dispatch();
 }
+
+function show_label_volume_window(show) {
+       var logbox = document.getElementById('<%=$this->LabelVolumeLog->ClientID%>');
+       logbox.innerHTML = '';
+       var logbox_container = document.getElementById('label_volume_log');
+       logbox_container.style.display = 'none';
+       set_labeling_status('start');
+       document.getElementById('label_volume').style.display = show ? 'block' : 'none';
+}
+
+<%=$this->getBarcodeLabel() ? 'set_label_volume_barcodes(true);' : ''%>
 </script>
index 4a9b0b7bfc09f59d3532c98e3de749bf443686b1..0247415df888dca5defa284437bee746bf477243 100644 (file)
@@ -3,7 +3,7 @@
  * Bacula(R) - The Network Backup Solution
  * Baculum   - Bacula web interface
  *
- * Copyright (C) 2013-2019 Kern Sibbald
+ * Copyright (C) 2013-2021 Kern Sibbald
  *
  * The main author of Baculum is Marcin Haba.
  * The original author of Bacula is Kern Sibbald, with contributions
@@ -35,6 +35,10 @@ Prado::using('Application.Web.Portlets.Portlets');
  */
 class UpdateSlots extends Portlets {
 
+       const SHOW_BUTTON = 'ShowButton';
+       const BARCODE_UPDATE = 'BarcodeUpdate';
+       const STORAGE = 'Storage';
+
        public function loadValues() {
                $storages = $this->getModule('api')->get(array('storages'));
                $storage_list = array();
@@ -43,13 +47,17 @@ class UpdateSlots extends Portlets {
                                $storage_list[$storage->storageid] = $storage->name;
                        }
                }
-               $this->StorageUpdate->dataSource = $storage_list;
+               $this->StorageUpdate->DataSource = $storage_list;
+               if ($this->Storage) {
+                       $storage_list_flip =array_flip($storage_list);
+                       $this->StorageUpdate->SelectedValue = $storage_list_flip[$this->Storage];
+               }
                $this->StorageUpdate->dataBind();
        }
 
        public function update($sender, $param) {
                $url_params = array();
-               if($this->Barcodes->Checked == true) {
+               if($this->Barcodes->Checked == true || $this->BarcodeUpdate) {
                        $url_params = array('volumes', 'update', 'barcodes');
                } else {
                        $url_params = array('volumes', 'update');
@@ -69,11 +77,14 @@ class UpdateSlots extends Portlets {
                                $this->getPage()->getCallbackClient()->callClientFunction('update_slots_output_refresh', array($out->out_id));
                        }
                }
+
                if ($result->error === 0) {
                        $this->getPage()->getCallbackClient()->callClientFunction('set_updating_status', array('loading'));
                        $this->UpdateSlotsLog->Text = implode('', $result->output);
+                       $this->onUpdateStart($param);
                } else {
                        $this->UpdateSlotsLog->Text = $result->output;
+                       $this->onUpdateFail($param);
                }
        }
 
@@ -106,10 +117,59 @@ class UpdateSlots extends Portlets {
                                $this->getPage()->getCallbackClient()->callClientFunction('update_slots_output_refresh', array($out_id));
                        } else {
                                $this->getPage()->getCallbackClient()->callClientFunction('set_updating_status', array('finish'));
+                               $this->onUpdateSuccess($param);
+                               $this->onUpdateComplete($param);
                        }
                } else {
                        $this->UpdateSlotsLog->Text = $result->output;
+                       $this->onUpdateFail($param);
+                       $this->onUpdateComplete($param);
                }
        }
+       public function onUpdateStart($param) {
+               $this->raiseEvent('OnUpdateStart', $this, $param);
+       }
+
+       public function onUpdateComplete($param) {
+               $this->raiseEvent('OnUpdateComplete', $this, $param);
+       }
+
+       public function onUpdateSuccess($param) {
+               $this->raiseEvent('OnUpdateSuccess', $this, $param);
+       }
+
+       public function onUpdateFail($param) {
+               $this->raiseEvent('OnUpdateFail', $this, $param);
+       }
+
+       public function setSlots(array $slots) {
+               $this->SlotsUpdate->Text = implode(',', $slots);
+       }
+
+       public function setShowButton($show) {
+               $show = TPropertyValue::ensureBoolean($show);
+               $this->setViewState(self::SHOW_BUTTON, $show);
+       }
+
+       public function getShowButton() {
+               return $this->getViewState(self::SHOW_BUTTON, true);
+       }
+
+       public function setBarcodeUpdate($barcode_update) {
+               $barcode_update = TPropertyValue::ensureBoolean($barcode_update);
+               $this->setViewState(self::BARCODE_UPDATE, $barcode_update);
+       }
+
+       public function getBarcodeUpdate() {
+               return $this->getViewState(self::BARCODE_UPDATE);
+       }
+
+       public function setStorage($storage) {
+               $this->setViewState(self::STORAGE, $storage);
+       }
+
+       public function getStorage() {
+               return $this->getViewState(self::STORAGE);
+       }
 }
 ?>
index aaca9d07a8f95eea7f71bc919a576cbc9e335d50..36d5c9000788927209bc53994d7048bb757fa50b 100644 (file)
@@ -1,14 +1,9 @@
 <com:TActiveLinkButton
        CssClass="w3-button w3-green"
        OnClick="loadValues"
+       Visible="<%=$this->ShowButton%>"
 >
        <prop:Attributes.onclick>
-               var logbox = document.getElementById('<%=$this->UpdateSlotsLog->ClientID%>');
-               logbox.innerHTML = '';
-               var logbox_container = document.getElementById('update_slots_log');
-               logbox_container.style.display = 'none';
-               set_updating_status('start');
-               document.getElementById('update_slots').style.display = 'block';
        </prop:Attributes.onclick>
        <i class="fa fa-retweet"></i> &nbsp;<%[ Update slots ]%>
 </com:TActiveLinkButton>
@@ -48,9 +43,9 @@
                        />
                        <div class="w3-row-padding w3-section-padding w3-section">
                                <div class="w3-col w3-half"><com:TLabel ForControl="Barcodes" Text="<%[ Update slots using barcodes ]%>" /></div>
-                               <div class="w3-col w3-half"><com:TActiveCheckBox ID="Barcodes" CssClass="w3-check" Checked="true" /></div>
+                               <div class="w3-col w3-half"><com:TActiveCheckBox ID="Barcodes" CssClass="w3-check" Checked="true" Attributes.onclick="set_update_slots_barcodes();" /></div>
                        </div>
-                       <div class="w3-row-padding w3-section">
+                       <div class="w3-row-padding w3-section"<%=$this->Storage ? ' style="display: none"' : ''%>>
                                <div class="w3-col w3-half"><com:TLabel ForControl="StorageUpdate" Text="<%[ Storage: ]%>" /></div>
                                <div class="w3-col w3-half"><com:TActiveDropDownList ID="StorageUpdate" CssClass="w3-select w3-border" /></div>
                        </div>
 </com:TCallback>
 <script type="text/javascript">
 var update_slots_logbox_scroll = false;
+function set_update_slots_barcodes(force_barcodes) {
+       var chkb = document.getElementById('<%=$this->Barcodes->ClientID%>');
+       if (force_barcodes) {
+               chkb.checked = true;
+               chkb.setAttribute('disabled', 'disabled');
+       }
+}
+function set_update_slots(force) {
+       var chkb = document.getElementById('<%=$this->Barcodes->ClientID%>');
+       if (force) {
+               chkb.checked = false;
+               chkb.setAttribute('disabled', 'disabled');
+       }
+}
 function set_updating_status(status) {
        var start = document.getElementById('update_slots_status_start');
        var loading = document.getElementById('update_slots_status_loading');
@@ -165,4 +174,13 @@ function set_update_slots_output(out_id) {
        cb.setCallbackParameter(out_id);
        cb.dispatch();
 }
+function show_update_slots_window() {
+       var logbox = document.getElementById('<%=$this->UpdateSlotsLog->ClientID%>');
+       logbox.innerHTML = '';
+       var logbox_container = document.getElementById('update_slots_log');
+       logbox_container.style.display = 'none';
+       set_updating_status('start');
+       document.getElementById('update_slots').style.display = 'block';
+}
+<%=$this->getBarcodeUpdate() ? 'set_update_slots_barcodes(true);' : ''%>
 </script>