3 * Bacula(R) - The Network Backup Solution
4 * Baculum - Bacula web interface
6 * Copyright (C) 2013-2016 Kern Sibbald
8 * The main author of Baculum is Marcin Haba.
9 * The original author of Bacula is Kern Sibbald, with contributions
10 * from many others, a complete list can be found in the file AUTHORS.
12 * You may use this file and others of this release according to the
13 * license defined in the LICENSE file, which includes the Affero General
14 * Public License, v3.0 ("AGPLv3") and some additional permissions and
15 * terms pursuant to its AGPLv3 Section 7.
17 * This notice must be preserved when any source code is
18 * conveyed and/or propagated.
20 * Bacula(R) is a registered trademark of Kern Sibbald.
23 Prado::using('Application.Common.Class.Params');
24 Prado::using('Application.Common.Class.Errors');
25 Prado::using('Application.API.Class.BException');
26 Prado::using('Application.API.Class.APIModule');
27 Prado::using('Application.API.Class.APIConfig');
30 * Read/write Bacula configuration.
32 * @author Marcin Haba <marcin.haba@bacula.pl>
34 class BaculaSetting extends APIModule {
36 const COMPONENT_DIR_TYPE = 'dir';
37 const COMPONENT_SD_TYPE = 'sd';
38 const COMPONENT_FD_TYPE = 'fd';
39 const COMPONENT_BCONS_TYPE = 'bcons';
41 private function getComponentTypes() {
43 self::COMPONENT_DIR_TYPE,
44 self::COMPONENT_SD_TYPE,
45 self::COMPONENT_FD_TYPE,
46 self::COMPONENT_BCONS_TYPE
51 public function getConfig($component_type = null, $resource_type = null, $resource_name = null) {
52 $this->checkConfigSupport($component_type);
54 $json_tools = $this->Application->getModule('json_tools');
55 if (!is_null($component_type)) {
56 // get resources config
58 if ($component_type == self::COMPONENT_DIR_TYPE) {
59 $params['dont_apply_jobdefs'] = true;
61 if (!is_null($resource_type)) {
62 $params['resource_type'] = $resource_type;
64 if (!is_null($resource_name)) {
65 $params['resource_name'] = $resource_name;
67 $config = $json_tools->execCommand($component_type, $params);
69 // get components config
70 $config = $this->getComponents();
75 private function getComponents() {
76 $components_info = array();
77 $json_tools = $this->Application->getModule('json_tools');
78 $components = $this->getSupportedComponents();
80 for ($i = 0; $i < count($components); $i++) {
81 $component_type = $components[$i];
84 $resource_type = $this->Application->getModule('misc')->getMainComponentResource($component_type);
85 $directive_name = 'Name';
87 'resource_type' => $resource_type,
88 'directive_name' => $directive_name,
91 $result = $json_tools->execCommand($component_type, $params);
92 $state = ($result['exitcode'] === 0 && is_array($result['output']));
93 if ($state === true) {
95 if (count($result['output']) > 0) {
96 $component_directive = array_pop($result['output']);
97 if (array_key_exists($directive_name, $component_directive)) {
98 $component_name = $component_directive[$directive_name];
103 * Unable to get component info (tool returned an error).
104 * Keep error message and continue with rest components.
106 $error_msg = $result['output'];
109 'component_type' => $component_type,
110 'component_name' => $component_name,
112 'error_msg' => $error_msg
114 array_push($components_info, $component);
117 $error = BaculaConfigError::ERROR_NO_ERRORS;
118 if ($is_any === false) {
119 $error = BaculaConfigError::ERROR_CONFIG_NO_JSONTOOL_READY;
121 $result = $json_tools->prepareResult($components_info, $error);
125 public function setConfig($config, $component_type, $resource_type = null, $resource_name = null) {
126 $ret = array('is_valid' => false, 'save_result' => false, 'result' => null);
127 $this->checkConfigSupport($component_type);
128 $json_tools = $this->Application->getModule('json_tools');
130 if ($component_type == self::COMPONENT_DIR_TYPE) {
131 $params['dont_apply_jobdefs'] = true;
133 $result = $json_tools->execCommand($component_type, $params);
134 if ($result['exitcode'] === 0 && is_array($result['output'])) {
135 $config_orig = $result['output'];
136 if (!is_null($resource_type)) {
137 // Set single resource
138 $config_new = array($resource_type => $config);
141 $config_new = $config;
143 $ret = $this->saveConfig($config_orig, $config_new, $component_type, $resource_type, $resource_name);
148 private function saveConfig(array $config_orig, array $config_new, $component_type, $resource_type = null, $resource_name = null) {
151 if (!is_null($resource_type) && !is_null($resource_name)) {
152 // Update single resource in config
153 $config = $this->updateConfigResource($config_orig, $config_new, $resource_type, $resource_name);
154 } elseif (count($config_orig) > 0) {
155 // Update whole config
156 $config = $this->updateConfig($config_orig, $config_new);
157 } elseif (count($config_new) > 0) {
158 // Add new config (create component config)
159 $config = $config_new;
162 // Save config to file
163 return $this->getModule('bacula_config')->setConfig($component_type, $config);
166 private function updateConfig(array $config_orig, array $config_new) {
168 for ($i = 0; $i < count($config_orig); $i++) {
169 $resource_orig = $config_orig[$i];
170 for ($j = 0; $j < count($config_new); $j++) {
171 $resource_new = $config_new[$j];
172 if ($this->compareResources(array($resource_orig, $resource_new)) === true) {
173 // Resource type and name are the same. Update directives.
174 $config[] = $this->updateResource($resource_orig, $resource_new);
176 // Rewrite not modified resource
177 $config[] = $resource_new;
184 private function updateConfigResource(array $config_orig, array $resource, $resource_type, $resource_name) {
187 for ($i = 0; $i < count($config_orig); $i++) {
188 $resource_orig = $config_orig[$i];
189 if ($this->compareResources(array($resource_orig, $resource)) === true) {
190 // Resource type and name are the same. Update directives.
191 $config[] = $this->updateResource($resource_orig, $resource);
194 // Rewrite not modified resource
195 $config[] = $this->updateResource($resource_orig, $resource_orig);
198 if ($is_update === false) {
199 // Existing resource with changed name, or new resource
200 $resource_index = $this->getConfigResourceIndex($config, $resource_type, $resource_name);
201 if (!is_null($resource_index)) {
203 $resource_orig = $config[$resource_index];
204 // Remove existing resource
205 array_splice($config, $resource_index, 1);
206 // Add resource with new name
207 $config[] = $this->updateResource($resource_orig, $resource);
210 $config[] = $this->updateResource(array($resource_type => array()), $resource);
216 private function updateResource(array $resource_orig, array $resource_new) {
218 $resource_type_orig = key($resource_orig);
219 $resource_type_new = key($resource_new);
221 if ($resource_type_new === 'Schedule') {
222 $resource_type = $resource_type_new;
223 $resource = array($resource_type => array());
224 foreach ($resource_new[$resource_type] as $directive_name => $directive_value) {
225 if ($directive_name === 'Run') {
226 for($i = 0; $i < count($directive_value); $i++) {
227 if (is_array($directive_value[$i])) {
228 $overwrite_directive = array_map('overwrite_directives_callback', array_keys($directive_value[$i]), array_values($directive_value[$i]));
229 $overwrite_directive = implode(' ', array_filter($overwrite_directive));
230 $hour = $directive_value[$i]['Hour'][0];
232 if (count($directive_value[$i]['Hour']) === 24) {
237 * Check if Minute key exists because of bug about missing Minute
238 * @see http://bugs.bacula.org/view.php?id=2318
240 if (array_key_exists('Minute', $directive_value[$i])) {
241 $minute = sprintf('%02d', $directive_value[$i]['Minute']);
243 $day = count($directive_value[$i]['Day']) === 31 ? '' : 'on ' . implode(',', $directive_value[$i]['Day']);
244 $month = Params::getMonthsConfig($directive_value[$i]['Month']);
245 $week = Params::getWeeksConfig($directive_value[$i]['WeekOfMonth']);
246 $wday = Params::getWdaysConfig($directive_value[$i]['DayOfWeek']);
247 $value = array($overwrite_directive, $month, $week, $day, $wday, $hourly, 'at', "$hour:$minute");
248 $value = array_filter($value);
249 if (!array_key_exists($directive_name, $resource[$resource_type])) {
250 $resource[$resource_type][$directive_name] = array();
252 $resource[$resource_type][$directive_name][] = implode(' ', $value);
254 $resource[$resource_type][$directive_name][] = $directive_value[$i];
258 $resource[$resource_type][$directive_name] = $this->formatDirectiveValue($directive_value);
262 } elseif ($resource_type_new === 'Messages') {
263 $resource_type = $resource_type_new;
264 $resource = array($resource_type => array());
265 foreach ($resource_new[$resource_type] as $directive_name => $directive_value) {
266 if ($directive_name === 'Destinations') {
267 for ($i = 0; $i < count($directive_value); $i++) {
269 if (array_key_exists('Where', $directive_value[$i])) {
270 array_push($value, implode(',', $directive_value[$i]['Where']));
272 array_push($value, implode(', ', $directive_value[$i]['MsgTypes']));
273 $resource[$resource_type][$directive_value[$i]['Type']] = implode(' = ', $value);
276 $resource[$resource_type][$directive_name] = $directive_value;
280 } elseif ($resource_type_orig === $resource_type_new) {
281 $resource_type = $resource_type_orig;
282 $resource = array($resource_type => array());
284 foreach ($resource_orig[$resource_type] as $directive_name => $directive_value) {
285 if (!array_key_exists($directive_name, $resource_new[$resource_type])) {
286 // directive removed in resource
289 if (is_array($resource_new[$resource_type][$directive_name])) {
290 // nested directive (name { key = val })
291 $resource[$resource_type][$directive_name] = $this->updateSubResource($resource_new[$resource_type][$directive_name]);
293 // simple directive (key=val)
294 // existing directive in resource
295 $resource[$resource_type][$directive_name] = $this->formatDirectiveValue($resource_new[$resource_type][$directive_name]);
298 foreach ($resource_new[$resource_type] as $directive_name => $directive_value) {
299 if (!array_key_exists($directive_name, $resource_orig[$resource_type])) {
300 // new directive in resource
301 $resource[$resource_type][$directive_name] = $this->formatDirectiveValue($directive_value);
305 // It shouldn't happen.
306 $this->getModule('logging')->log(
308 "Attemp to update resource with different resource types.",
309 Logging::CATEGORY_APPLICATION,
313 $resource = $resource_orig;
318 private function updateSubResource(array $subresource_new) {
320 foreach($subresource_new as $directive_name => $directive_value) {
321 $check_recursive = false;
322 if (is_array($directive_value)) {
323 $assoc_keys = array_filter(array_keys($directive_value), 'is_string');
324 $check_recursive = count($assoc_keys) > 0;
326 if ($check_recursive === true) {
327 $resource[$directive_name] = $this->updateSubResource($directive_value);
329 $resource[$directive_name] = $this->formatDirectiveValue($directive_value);
336 private function compareResources(array $resources) {
337 $same_resource = false;
338 $items = array('type' => array(), 'name' => array());
339 $resources_count = count($resources);
341 for ($i = 0; $i < $resources_count; $i++) {
342 if (count($resources[$i]) === 1) {
343 $resource_type = key($resources[$i]);
344 if (array_key_exists('Name', $resources[$i][$resource_type])) {
345 $items['type'][] = $resource_type;
346 $items['name'][] = $resources[$i][$resource_type]['Name'];
351 if ($resources_count > 1 && $resources_count === $counter) {
353 foreach ($items as $key => $value) {
354 $result = (count(array_unique($value)) === 1);
355 if ($result === false) {
359 $same_resource = $result;
361 return $same_resource;
364 private function getConfigResourceIndex($config, $resource_type, $resource_name) {
366 $find_resource = array($resource_type => array('Name' => $resource_name));
367 for ($i = 0; $i < count($config); $i++) {
368 if ($this->compareResources(array($config[$i], $find_resource)) === true) {
377 * Format directive value.
378 * It is used on write config action to last prepare config before
379 * sending it to config writer.
381 * @param mixed $value directive value
382 * @return mixed formatted directive value
384 private function formatDirectiveValue($value) {
385 $directive_value = null;
386 if (is_bool($value)) {
387 $directive_value = ($value === true) ? 'yes' : 'no';
388 } elseif (is_int($value)) {
389 $directive_value = $value;
390 } elseif (is_string($value)) {
391 $value = str_replace('"', '\"', $value);
392 $value = "\"$value\"";
393 $directive_value = $value;
394 } elseif (is_array($value)) {
395 // only simple numeric arrays
397 for ($i = 0; $i < count($value); $i++) {
398 if (is_array($value[$i])) {
399 $dvalues[] = $this->updateSubResource($value[$i]);
401 $dvalues[] = $this->formatDirectiveValue($value[$i]);
404 $directive_value = $dvalues;
406 $emsg = sprintf("Attemp to format a directive value with not supported value type '%s'.", gettype($value));
407 $this->getModule('logging')->log(
410 Logging::CATEGORY_APPLICATION,
415 return $directive_value;
419 * Get supported component types.
420 * The support is determined by configured JSON tool in API config.
421 * If API is able to use JSON tool for specific component then the component is supported.
422 * Currently a component type is the same as related JSON tool type, but it can be
423 * changed in the future. From this reason components have theirown types.
425 * @return array supported component types
426 * @throws BConfigException if json tools support is disabled
428 public function getSupportedComponents() {
429 $components = array();
430 $types = $this->getComponentTypes();
431 $tools = $this->getModule('api_config')->getSupportedJSONTools();
432 for ($i = 0; $i < count($tools); $i++) {
433 if (in_array($tools[$i], $types)) {
434 array_push($components, $tools[$i]);
441 * Check if config support is configured and enabled
442 * globally and for specific component type.
445 * @param mixed $component_type component type for which config support is checked
446 * @throws BConfigException if support is not configured or disabled
448 private function checkConfigSupport($component_type = null) {
449 $api_cfg = $this->getModule('api_config');
450 if (!$api_cfg->isJSONToolsConfigured($component_type) || !$api_cfg->isJSONToolsEnabled()) {
451 throw new BConfigException(
452 JSONToolsError::MSG_ERROR_JSON_TOOLS_DISABLED,
453 JSONToolsError::ERROR_JSON_TOOLS_DISABLED
455 } elseif (!is_null($component_type) && !$api_cfg->isJSONToolConfigured($component_type)) {
456 $emsg = ' JSONTool=>' . $component_type;
457 throw new BConfigException(
458 JSONToolsError::MSG_ERROR_JSON_TOOL_NOT_CONFIGURED . $emsg,
459 JSONToolsError::ERROR_JSON_TOOLS_DISABLED
465 * Get JSON tool type by component type.
466 * Currently the mapping is one-to-one because each component type is the same
467 * as json tool type (dir == dir, bcons == bcons ...etc.). The method is for
468 * hypothetical case when component type is different than json tool type.
469 * It can be useful in future.
471 * @param string $component_type component type
472 * @return string json tool type
474 public function getJSONToolTypeByComponentType($component_type) {
476 switch ($component_type) {
477 case self::COMPONENT_DIR_TYPE: $tool_type = APIConfig::JSON_TOOL_DIR_TYPE; break;
478 case self::COMPONENT_SD_TYPE: $tool_type = APIConfig::JSON_TOOL_SD_TYPE; break;
479 case self::COMPONENT_FD_TYPE: $tool_type = APIConfig::JSON_TOOL_FD_TYPE; break;
480 case self::COMPONENT_BCONS_TYPE: $tool_type = APIConfig::JSON_TOOL_BCONS_TYPE; break;
486 public function testConfigDir($path) {
487 $valid = is_writable($path);
491 function overwrite_directives_callback($directive_name, $directive_value) {
493 $overwrite_directives = array(
508 if (in_array($directive_name, $overwrite_directives)) {
509 $directive = "{$directive_name}={$directive_value}";