diff --git a/almir/__init__.py b/almir/__init__.py
index 2375a19..590745d 100644
--- a/almir/__init__.py
+++ b/almir/__init__.py
@@ -102,6 +102,11 @@ def main(global_config, **settings):
renderer='json',
request_method='GET')
+ config.add_route('modify_storage', '/storage/modify')
+ config.add_view('almir.views.modify_storage',
+ route_name='modify_storage',
+ request_method='POST')
+
# exception handling views
config.add_view('almir.views.httpexception',
context=HTTPError,
diff --git a/almir/forms.py b/almir/forms.py
index 197958c..917eb4e 100644
--- a/almir/forms.py
+++ b/almir/forms.py
@@ -6,6 +6,7 @@
job_states = (
('finished+running', 'Finished + Running'),
('scheduled', 'Scheduled'),
+ ('disabled', 'Disabled'),
)
diff --git a/almir/lib/bconsole.py b/almir/lib/bconsole.py
index a12f088..3724622 100644
--- a/almir/lib/bconsole.py
+++ b/almir/lib/bconsole.py
@@ -7,7 +7,11 @@
from contextlib import contextmanager
from subprocess import Popen, PIPE
-from almir.lib.utils import nl2br
+#from almir.lib.utils import nl2br
+from utils import nl2br
+
+import logging
+log = logging.getLogger(__name__)
CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
@@ -65,6 +69,21 @@ def from_temp_config(cls, name, address, port, password):
def start_process(self):
return Popen(shlex.split(self.bconsole_command), stdout=PIPE, stdin=PIPE, stderr=PIPE)
+ def send_command(self, cmd):
+ log.debug('Sending command to bconsole: %s' % cmd)
+ p = self.start_process()
+ stdout, stderr = p.communicate(cmd)
+ log.debug('Command output by bconsole:')
+ log.debug(stdout)
+ # cleaning stdout from connection info
+ # removing firsts four lines
+# for i in range(3):
+# stdout = stdout[:stdout.find('\n')]
+
+ # removing you have messages. msg
+ stdout = stdout.replace('You have messages.\n','')
+ return stdout
+
def is_running(self):
try:
self.get_version()
@@ -73,8 +92,8 @@ def is_running(self):
return False
def get_version(self):
- p = self.start_process()
- stdout, stderr = p.communicate('version\n')
+ stdout = self.send_command('version\n')
+
version = filter(lambda s: 'Version' in s, stdout.split('\n'))
if version:
return version[-1]
@@ -82,8 +101,8 @@ def get_version(self):
raise DirectorNotRunning
def get_jobs_settings(self):
- p = self.start_process()
- stdout, stderr = p.communicate('show job')
+ stdout = self.send_command('show job\n')
+
jobs = []
for job in stdout.split('Job:'):
jobs.append(JOBS_DEF_RE.find(stdout))
@@ -111,18 +130,43 @@ def make_backup(self, job, level=None, storage=None, fileset=None, client=None,
if when:
cmd += " when=%s" % when
- p = self.start_process()
- stdout, stderr = p.communicate(cmd + "\nyes\n")
+ stdout = self.send_command(cmd + "\nyes\n")
+
if True:
return "jobid"
else:
# TODO: stderr why job failed?
return False
+ def get_disabled_jobs(self):
+ # get the list of disabled jobs
+ stdout = self.send_command('show disabled\n')
+
+# example of header:
+# Disabled Jobs:
+# BackupCatalog
+#
+
+
+ try:
+ unparsed_jobs = stdout.split('Disabled Jobs:\n')[1]
+ except IndexError:
+ return []
+
+ disabled=[x.strip() for x in unparsed_jobs.split('\n') if len(x)>1]
+
+ jobs = []
+
+ for job in disabled:
+ jobs.append({'name': job,
+ })
+
+ return jobs
+
def get_upcoming_jobs(self, days=1):
""""""
- p = self.start_process()
- stdout, stderr = p.communicate('.status dir scheduled days=%d\n' % days)
+
+ stdout = self.send_command('.status dir scheduled days=%d\n' % days)
#if stderr.strip():
# pass # TODO: display flash?
@@ -133,6 +177,7 @@ def get_upcoming_jobs(self, days=1):
return []
jobs = []
+
for line in unparsed_jobs.split('\n'):
if not line.strip():
continue
@@ -149,6 +194,126 @@ def get_upcoming_jobs(self, days=1):
return jobs
+
+ def mount_storage(self, storage, slot=0):
+ """Mounts the volume contained in the slot *slot* on the storage *storage*"""
+
+ cmd = 'mount=%s slot=%d\n' % (storage,slot)
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('is mounted')
+
+ return is_ok != -1
+
+
+ def unmount_storage(self, storage):
+ """Unmounts the storage *storage*"""
+ cmd = 'unmount=%s \n' % storage
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('unmounted')
+
+ return is_ok != -1
+
+
+ def release(self, storage):
+ """Releases the storage *storage*"""
+ cmd = 'release=%s \n' % storage
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('released')
+
+ return is_ok != -1
+
+
+ def update_slots(self):
+ """Update slots"""
+ cmd = 'update slots\n'
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('error')
+
+ return is_ok == -1
+
+
+ def delete(self, volume=None, jobid=None):
+ """Deletes an object"""
+
+ if not volume and not jobid:
+ return False # what you want to delete?
+
+ if volume:
+ cmd = ' volume=%s\nyes' % (volume)
+ if jobid:
+ cmd = ' jobid=%d ' % (jobid)
+
+ cmd = 'delete %s \n' % cmd
+
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('deleted')
+
+ return is_ok != -1
+
+ def create_label(self, pool, storage='', label = None, barcode = False ):
+ """Create a new label"""
+
+ if not label and not barcode:
+ return False # we need or manual label or barcode
+
+ cmd = 'label pool=%s storage=%s' % (pool,storage)
+
+ if barcode:
+ cmd += ( " barcode\n" )
+ else:
+ cmd += ( "\n%s\n" % label )
+
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('successfully created')
+
+ return is_ok != -1
+
+
+ def enable_job(self, jobname ):
+ """Enables job named as passed by argument"""
+
+ cmd = 'enable job=%s\n' % jobname
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('enabled')
+
+ return is_ok != -1
+
+
+ def disable_job(self, jobname):
+ """Disables job named as passed by argument"""
+
+ cmd = 'disable job=%s\n' % jobname
+ stdout = self.send_command(cmd)
+
+ is_ok = stdout.find('disabled')
+
+ return is_ok != -1
+
+
+ def estimate_job(self, jobname ):
+ """Estimates a job returns -1,-1 if something goes wrong"""
+
+ cmd = 'estimate job=%s\n' % jobname
+ stdout = self.send_command(cmd)
+
+ try:
+ retcode, files, bytes = re.findall('\\d+',stdout.replace(',',''))
+ except ValueError:
+ retcode = -1
+
+ if int(retcode) != 2000:
+ return -1,-1
+ else:
+ return int(files), int(bytes)
+
+
def send_command_by_polling(self, command, process=None):
""""""
if command == 'quit':
@@ -185,3 +350,4 @@ def send_command_by_polling(self, command, process=None):
output = nl2br(output)
return process, {"commands": [output]}
+
diff --git a/almir/models.py b/almir/models.py
index 523f5de..4164ee2 100644
--- a/almir/models.py
+++ b/almir/models.py
@@ -227,6 +227,10 @@ def get_list(cls, **kw):
if appstruct and appstruct['state'] == 'scheduled':
return BConsole().get_upcoming_jobs()
+
+ if appstruct and appstruct['state'] == 'disabled':
+ return BConsole().get_disabled_jobs()
+
query = cls.query.options(joinedload(cls.status), joinedload(cls.client))
if appstruct:
if appstruct['status']:
diff --git a/almir/static/js/main.js b/almir/static/js/main.js
index 9c40d93..6dabef7 100644
--- a/almir/static/js/main.js
+++ b/almir/static/js/main.js
@@ -88,7 +88,7 @@ $(function () {
},
"oTableTools": {
sSwfPath: tabletools_swf,
- aButtons: ["copy", "xls", "pdf", "print"]
+ aButtons: ["copy", "csv", "pdf", "print"]
},
"iDisplayLength": 50,
"bDestroy": true,
@@ -99,11 +99,11 @@ $(function () {
href;
// clickable-row
href = $this.find('td:first a').attr('href');
- if (typeof href == 'string') {
+ /*if (typeof href == 'string') {
$this.addClass('clickable-row').click(function (e) {
window.location = href;
});
- }
+ } commented out.. is not usable when there are dropboxes in the rows, we can adjust it?*/
});
}
};
diff --git a/almir/templates/base.jinja2 b/almir/templates/base.jinja2
index 2d12b46..14ae5de 100644
--- a/almir/templates/base.jinja2
+++ b/almir/templates/base.jinja2
@@ -48,9 +48,11 @@
{% endblock %}
+
+
Almir
{% for item in navigation_tree %}
+
+
+ {% if request.params.get('okmsg') %}
+
Ok: {{ request.params.get('okmsg') }}
+ {% endif %}
+
+ {% if request.params.get('errormsg') %}
+
Error: {{ request.params.get('errormsg') }}
+ {% endif %}
+
+
{% block container %}
diff --git a/almir/templates/job_list.jinja2 b/almir/templates/job_list.jinja2
index af1547b..888b9fe 100644
--- a/almir/templates/job_list.jinja2
+++ b/almir/templates/job_list.jinja2
@@ -2,8 +2,13 @@
{% block content %}
Job list
+
{% if appstruct.state == 'scheduled' %}
+
{{ macros.upcoming_jobs_table(request, objects) }}
+ {% elif appstruct.state == 'disabled' %}
+
+ {{ macros.disabled_jobs_table(request, objects) }}
{% else %}
diff --git a/almir/templates/macros.jinja2 b/almir/templates/macros.jinja2
index e2e0374..78f3694 100644
--- a/almir/templates/macros.jinja2
+++ b/almir/templates/macros.jinja2
@@ -26,6 +26,7 @@
Priority |
Scheduled |
Volume |
+
Select |
@@ -37,12 +38,37 @@
{{ job.priority }} |
{{ job.date }} {{ job.time }} |
{{ job.volume }} |
+ |
{% endfor %}
+
{%- endmacro %}
+
+{% macro disabled_jobs_table(request, jobs) -%}
+
+
+{%- endmacro %}
+
+
+
{% macro jobs_table(request, jobs) -%}
diff --git a/almir/templates/storage_list.jinja2 b/almir/templates/storage_list.jinja2
index 95f8e04..e33ed17 100644
--- a/almir/templates/storage_list.jinja2
+++ b/almir/templates/storage_list.jinja2
@@ -3,22 +3,34 @@
{% block container %}
Storage list
By name
-
+
+
+
{% endblock %}
diff --git a/almir/tests/test_bconsole.py b/almir/tests/test_bconsole.py
index 400b391..261b270 100644
--- a/almir/tests/test_bconsole.py
+++ b/almir/tests/test_bconsole.py
@@ -21,6 +21,7 @@ def test_is_running(self):
start_process.communicate.return_value = ('error', 'error')
self.assertFalse(b.is_running())
+
def test_from_temp_config(self):
output = \
"""# generated by almir, you should never edit this file. Do:
@@ -82,6 +83,20 @@ def test_get_upcoming_jobs(self):
'type': 'Backup',
'volume': '*unknown*'}])
+
+ def test_get_disabled_jobs(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ("""Disabled Jobs:
+ Backupclient1
+""", '')
+
+ jobs = b.get_disabled_jobs()
+ self.assertEqual(jobs, [{'name': 'Backupclient1'}])
+
+
+
def test_send_command_by_polling(self):
b = BConsole()
with patch.object(b, 'start_process') as mock_method:
@@ -101,3 +116,111 @@ def test_send_command_by_polling(self):
process, outputs = b.send_command_by_polling('quit')
self.assertEqual(outputs['commands'][0], 'Try harder.')
+
+
+
+
+ def test_mount_storage(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('is mounted', '')
+ self.assertTrue(b.mount_storage('File',0))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertFalse(b.mount_storage('File',0))
+
+ def test_unmount_storage(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('unmounted.', '')
+ self.assertTrue(b.unmount_storage('File'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertFalse(b.unmount_storage('File'))
+
+ def test_delete(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('deleted', '')
+ self.assertTrue(b.delete(volume='test'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('deleted', '')
+ self.assertTrue(b.delete(jobid=1))
+
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertFalse(b.delete(volume='test'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertFalse(b.delete(jobid=1))
+
+ def test_create_label(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('successfully created', '')
+ self.assertTrue(b.create_label(pool='test', label = 'testlabel'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('successfully created', '')
+ self.assertTrue(b.create_label(pool= 'test' , storage='test', label = 'testlabel'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('successfully created', '')
+ self.assertTrue(b.create_label(pool='test', label = 'testlabel' ))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('successfully created', '')
+ self.assertTrue(b.create_label(pool='test', barcode = True))
+
+ def test_disable_job(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('disabled', '')
+ self.assertTrue(b.disable_job('testjob'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertFalse(b.disable_job('testjob'))
+
+ def test_enable_job(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('enabled', '')
+ self.assertTrue(b.enable_job('testjob'))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertFalse(b.enable_job('testjob'))
+
+ def test_estimate_job(self):
+ b = BConsole()
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ("2000 OK estimate files=1000 bytes=10,000,000", '')
+ self.assertTrue(b.estimate_job('testjob')==(1000,10000000))
+
+ with patch.object(b, 'start_process') as mock_method:
+ start_process = mock_method.return_value
+ start_process.communicate.return_value = ('error', 'error')
+ self.assertTrue(b.estimate_job('testjob')==(-1,-1))
diff --git a/almir/views.py b/almir/views.py
index 492323b..8a63be3 100644
--- a/almir/views.py
+++ b/almir/views.py
@@ -4,6 +4,9 @@
from deform import Form, ValidationFailure
from pyramid.interfaces import IRoutesMapper
from pyramid.settings import asbool
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPMovedPermanently, HTTPFound
+
from sqlalchemy import String, LargeBinary
from sqlalchemy.sql.expression import desc, or_
from sqlalchemy.sql.functions import sum, count
@@ -296,3 +299,40 @@ def datatables(request):
'iTotalRecords': total_records,
'aaData': aaData,
}
+
+
+def modify_storage(request):
+ bc = BConsole()
+
+ enderer='templates/storage_list.jinja2',
+
+ storages = ['File',]
+
+ log.debug('###################################')
+ log.debug(request.params)
+ log.debug('###################################')
+
+ command = request.POST.get('command')
+ if command == 'unmount':
+ ret = {'okmsg':'Storage unmounted correctly'}
+ for storage in storages:
+ if not bc.unmount_storage(storage):
+ msg = 'Error unmounting storage: %s' % storage
+ log.info(msg)
+ ret = {'errormsg':msg}
+ break
+ else:
+ ret = {'okmsg':'Storage mounted correctly'}
+ for storage in storages:
+ if not bc.mount_storage(storage):
+ msg = 'Error mounting storage: %s' % storage
+ log.info(msg)
+ ret = {'errormsg':msg}
+ break
+
+ redirect_url = request.route_url('storage_list', _query=ret)
+ response = HTTPFound(location=redirect_url)
+
+ return response
+
+