add show diff single

add apply diff single
This commit is contained in:
Andreas Stuerz
2021-02-25 17:02:55 +01:00
parent 4a88b924af
commit 2f888b6092
4 changed files with 418 additions and 62 deletions
@@ -41,6 +41,18 @@ use OPNsense\HAProxy\HAProxy;
*/
class MaintenanceController extends ApiControllerBase
{
/**
* jQuery bootstrap certificates diff list
* @return array|mixed
*/
public function searchCertificateDiffAction()
{
return $this->getData(
["cert_diff_list"],
["rowCount", "current", "searchPhrase", "sort"]
);
}
/**
* jQuery bootstrap server list
* @return array|mixed
@@ -53,6 +65,42 @@ class MaintenanceController extends ApiControllerBase
);
}
/**
* sync certificate for frontends
* @return array|mixed
*/
public function certSyncAction()
{
return $this->syncCerts(
["cert_sync"],
["frontend_ids"]
);
}
/**
* show certificate diff for frontends
* @return array|mixed
*/
public function certDiffAction()
{
return $this->getData(
["cert_diff"],
["frontend_ids"]
);
}
/**
* show certificate actions for frontends
* @return array|mixed
*/
public function certActionsAction()
{
return $this->getData(
["cert_actions"],
["frontend_ids"]
);
}
/**
* set server weight
* @return array|mixed
@@ -145,7 +193,7 @@ class MaintenanceController extends ApiControllerBase
}
/**
* Executes a backend command to save data
* Executes a backend command which returns output on error
* @param array $command
* @param array $arguments
* @return array|string[]
@@ -167,4 +215,28 @@ class MaintenanceController extends ApiControllerBase
"message" => 'only accept POST Requests.'
];
}
/**
* Executes a ssl certificate sync
* @param array $command
* @param array $arguments
* @return array|string[]
*/
protected function syncCerts(array $command, array $arguments = [])
{
if ($this->request->isPost()) {
$output = $this->safeBackendCmd($command, $arguments);
$result = json_decode($output, true);
return [
"status" => "ok",
"result" => $result,
];
}
return [
"status" => 'unavailable',
"message" => 'only accept POST Requests.'
];
}
}
@@ -26,9 +26,114 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
#}
<script>
$( document ).ready(function() {
// grid-certificates
$("#grid-certificates").bootgrid('destroy');
var grid_certificates = $("#grid-certificates").UIBootgrid({
search: '/api/haproxy/maintenance/searchCertificateDiff',
options: {
ajax: true,
selection: true,
multiSelect: true,
keepSelection: true,
rowCount:[10,25,50,100,500,1000],
searchSettings: {
delay: 250,
characters: 1
},
formatters: {
"commands": function (column, row) {
buttons = ""
buttons += "<button type=\"button\" data-action=\"showDiff\" title=\"{{ lang._('Show diff between configured ssl certificates and certificates from HAProxy memory.') }}\" class=\"btn btn-xs btn-default\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-info-circle\"></span></button>"
buttons += " <button type=\"button\" data-action=\"applyDiff\" title=\"{{ lang._('Apply diff and sync certificates into HAProxy memory.') }}\" class=\"btn btn-xs btn-default\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-refresh\"></span></button>"
return buttons;
},
},
}
}).on("loaded.rs.jquery.bootgrid", function(){
grid_certificates.find("*[data-action=showDiff]").off().on("click", function(e) {
var row_id = $(this).data("row-id");
var frontend_ids = row_id;
var payload = {
'frontend_ids': frontend_ids,
};
$.post('/api/haproxy/maintenance/certDiff', payload, function(data) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_INFO,
title: "{{ lang._('Diff between configured and remote ssl certificates') }}",
message: `<pre>${data}</pre>`,
buttons: [{
label: '{{ lang._('Close') }}',
action: function(dialog){
dialog.close();
}
}]
});
});
});
grid_certificates.find("*[data-action=applyDiff]").off().on("click", function(e) {
var row_id = $(this).data("row-id");
var rows = $("#grid-certificates").bootgrid("getCurrentRows");
var row = rows.filter(function(row) {
return row.id == row_id;
})[0];
var requested_count = row.total_count;
var frontend_ids = row.id
var payload = {
'frontend_ids': frontend_ids,
};
$.post('/api/haproxy/maintenance/certActions', payload, function(data_actions) {
question = ''
question += `<pre>${data_actions}</pre>`;
question += '<b>{{ lang._('Apply ssl certificates to HaProxy?') }}</b></br></br>';
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
question,
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
$.post('/api/haproxy/maintenance/certSync', payload, function(data) {
modified_count = data.result.add_count + data.result.remove_count + data.result.update_count;
if (requested_count != modified_count) {
var error_msg = syncErrorMessage(data.result.modified, data.result.deleted);
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('Error applying ssl certificates to HAProxy') }}",
message: error_msg,
buttons: [{
label: '{{ lang._('Close') }}',
action: function(dialog){
dialog.close();
}
}]
});
}
$("#grid-certificates").bootgrid("reload");
});
});
});
});
grid_certificates.find("*[data-action=showDiffBulk]").off().on("click", function(e) {
var rows = $("#grid-certificates").bootgrid("getSelectedRows");
console.log('Show diff for multi')
});
grid_certificates.find("*[data-action=applyDiffBulk]").off().on("click", function(e) {
var rows = $("#grid-certificates").bootgrid("getSelectedRows");
console.log('Apply diff for multi')
});
});
// grid-status
$("#grid-status").bootgrid('destroy');
var grid_status = $("#grid-status").UIBootgrid({
search: '/api/haproxy/maintenance/searchServer',
@@ -45,7 +150,7 @@ POSSIBILITY OF SUCH DAMAGE.
formatters: {
"commands": function (column, row) {
buttons = ""
buttons += "<button type=\"button\" title=\"{{ lang._('Set administrative state to ready. Puts the server in normal mode.') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"ready\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-check\"></span></button>"
buttons += "<button type=\"button\" title=\"{{ lang._('Set administrative state to ready. Puts the server in normal mode.') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"ready\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-check\"></span></button>"
buttons += " <button type=\"button\" title=\"{{ lang._('Set administrative state to drain. Removes the server from load balancing but still allows it to be health checked and to accept new persistent connections') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"drain\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-sort-amount-desc\"></span></button>"
buttons += " <button type=\"button\" title=\"{{ lang._('Set administrative state to maintenance. Disables any traffic to the server as well as any health checks.') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"maint\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-wrench\"></span></button>"
buttons += " <button type=\"button\" title=\"{{ lang._('Change server weight.') }}\" class=\"btn btn-xs btn-default command-set-weight\" data-weight=\"" + row.weight + "\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-balance-scale\"></span></button>"
@@ -91,7 +196,6 @@ POSSIBILITY OF SUCH DAMAGE.
}
});
});
});
// set single - server weight
@@ -242,6 +346,7 @@ POSSIBILITY OF SUCH DAMAGE.
<ul class="nav nav-tabs" role="tablist" id="maintabs">
<li class="active"><a data-toggle="tab" href="#server"><b>{{ lang._('Server') }}</b></a></li>
<li><a data-toggle="tab" href="#ssl-certs"><b>{{ lang._('SSL Certificates') }}</b></a></li>
</ul>
<div class="content-box tab-content">
@@ -281,6 +386,33 @@ POSSIBILITY OF SUCH DAMAGE.
</tfoot>
</table>
</div>
<div id="ssl-certs" class="tab-pane fade in">
<!-- tab page "ssl-certs" -->
<table id="grid-certificates" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="id" data-type="string" data-identifier="true" data-visible="false">{{ lang._('id') }}</th>
<th data-column-id="frontend_name" data-type="string">{{ lang._('Public Service Name') }}</th>
<th data-column-id="update_count" data-type="string">{{ lang._('Update Certificates') }}</th>
<th data-column-id="add_count" data-type="string">{{ lang._('Add Certificates') }}</th>
<th data-column-id="remove_count" data-type="string">{{ lang._('Remove Certificates') }}</th>
<th data-column-id="commands" data-width="8em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="showDiff" title="{{ lang._('Show diff between configured ssl certificates and certificates from HAProxy memory.') }}" type="button" class="btn btn-xs btn-default"><span class="fa fa-info-circle"></span></button>
<button data-action="applyDiff" title="{{ lang._('Apply diff and sync certificates into HAProxy memory.') }}" type="button" class="btn btn-xs btn-default"><span class="fa fa-refresh"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{{ partial("layout_partials/base_dialog_processing") }}
@@ -17,7 +17,7 @@ from haproxy import cmds
class SyncWithTarget:
""" Base class for sync objects to a target """
def __init__(self, socket='/var/run/haproxy.socket'):
def __init__(self, socket='/var/run/haproxy.socket', **kwargs):
self.socket = socket
def _execute_remote_cmd(self, command_class, **command_args):
@@ -40,7 +40,7 @@ class SyncWithTarget:
class Diff(SyncWithTarget):
""" Represents a full diff to sync with remote """
def __init__(self, crt_lists=None):
def __init__(self, crt_lists=None, **kwargs):
super().__init__()
if crt_lists is None:
crt_lists = []
@@ -49,6 +49,13 @@ class Diff(SyncWithTarget):
self._status = self._get_status()
self._transactions = self._get_transactions()
self.output_format = kwargs['output']
self.page = kwargs['page']
self.page_rows = kwargs['page_rows']
self.search = kwargs['search']
self.sort_col = kwargs['sort_col']
self.sort_dir = kwargs['sort_dir']
@property
def diff(self):
return self._diff
@@ -65,13 +72,53 @@ class Diff(SyncWithTarget):
def status(self):
return self._status
def _calc_diff(self):
result = {}
for crt_list in self:
result[crt_list.frontend_id] = crt_list.diff
return result
def _get_bootgrid_output(self, rows):
""" Returns jquery bootgrid output """
args = {
"rows": rows,
"page": int(self.page) if self.page != None else 1,
"page_rows": int(self.page_rows) if self.page_rows != None else len(rows),
"search": self.search,
"sort_col": self.sort_col if self.sort_col else 'id',
"sort_dir": self.sort_dir,
}
def abort(self, output_format):
# search
if args['search']:
filtered_rows = []
for row in rows:
def inner(row):
for k, v in row.items():
if args['search'] in v:
return row
return None
match = inner(row)
if match:
filtered_rows.append(match)
rows = filtered_rows
# sort
rows.sort(key=lambda k: k[args['sort_col']], reverse=True if args['sort_dir'] == 'desc' else False)
# pager
total = len(rows)
pages = [rows[i:i + args['page_rows']] for i in range(0, total, args['page_rows'])]
if pages and (args['page'] > len(pages) or args['page'] < 1):
raise KeyError(f"Current page {args['page']} does not exist. Available pages: {len(pages)}")
page = pages[args['page'] - 1] if pages else []
return json.dumps({
"rows": page,
"total": total,
"rowCount": args['page_rows'],
"current": args['page']
})
def _calc_diff(self):
return [crt_list.diff for crt_list in self if crt_list.diff['total_count'] > 0]
def abort(self):
""" Abort transactions"""
aborted = []
for certfile in self.transactions:
@@ -83,10 +130,10 @@ class Diff(SyncWithTarget):
"output": output,
})
if output_format == 'json':
if self.output_format == 'json':
print(json.dumps({'abort': aborted}))
if output_format == 'raw':
if self.output_format == 'raw':
for item in aborted:
print(f"ABORT transaction: {item['cert']}")
print(f" {repr(item['output'])}")
@@ -117,12 +164,12 @@ class Diff(SyncWithTarget):
}
return status
def show_status(self, output_format):
def show_diff(self):
""" Shows current local and remote state """
if output_format == 'json':
if self.output_format == 'json':
print(json.dumps(self.status))
if output_format == 'raw':
if self.output_format == 'raw':
print("## STATUS ##")
for frontend_id, crt_list in self.status.items():
print(f"CRT_LIST: {crt_list['path']}")
@@ -140,14 +187,17 @@ class Diff(SyncWithTarget):
print(f" REMOTE: {cert['remote']}")
print()
def show_diff(self, output_format):
def show_actions(self):
""" Shows what will be synced to target """
if output_format == 'json':
if self.output_format == 'json':
print(json.dumps(self.diff))
if output_format == 'raw':
if self.output_format == 'bootgrid':
print(self._get_bootgrid_output(self.diff))
if self.output_format == 'raw':
print("## DIFF ##")
for frontend_id, diff in self.diff.items():
for diff in self.diff:
print(f"CRT LIST: {diff['path']}")
print(f" FRONTEND NAME: {diff['frontend_name']}")
print(f" FRONTEND ID: {diff['frontend_id']}")
@@ -157,34 +207,62 @@ class Diff(SyncWithTarget):
print(f" Serial: {update['meta']['Serial']}")
print(f" Issuer: {update['meta']['Issuer']}")
print(f" Subject: {update['meta']['Subject']}")
print()
else:
if not diff['update']:
print(f" CERT UPDATE: []")
print(f" CERT ADD : {diff['add']}")
print(f" CERT DEL : {diff['del']}")
def show_transactions(self, output_format):
if output_format == 'json':
for add in diff['add']:
print(f" CERT ADD:")
print(f" Cert: {add}")
print()
else:
if not diff['add']:
print(f" CERT ADD: []")
for remove in diff['remove']:
print(f" CERT REMOVE:")
print(f" Cert: {remove['certfile']}")
print(f" Serial: {remove['meta'].get('Serial', None)}")
print(f" Issuer: {remove['meta'].get('Issuer', None)}")
print(f" Subject: {remove['meta'].get('Subject', None)}")
print()
else:
if not diff['remove']:
print(f" CERT REMOVE: []")
def show_transactions(self):
if self.output_format == 'json':
print(json.dumps({'transactions': self.transactions}))
if output_format == 'raw':
if self.output_format == 'raw':
print("## OPEN TRANSACTIONS ##")
for cert in self.transactions:
print(cert)
def sync(self, output_format):
def sync(self):
""" Sync to target """
sync = {}
sync = {
'modified': [],
'deleted': [],
'add_count': 0,
'remove_count': 0,
'update_count': 0,
'del_count': 0,
}
certs_to_delete = []
for frontend_id, diff in self.diff.items():
sync[frontend_id] = {
for diff in self.diff:
sync_item = {
'frontend_name': diff['frontend_name'],
'frontend_id': diff['frontend_id'],
'path': diff['path'],
'add': [],
'remove': [],
'update': [],
'del': []
'add_count': 0,
'remove_count': 0,
'update_count': 0,
}
# update cert content
@@ -200,48 +278,60 @@ class Diff(SyncWithTarget):
output = self._execute_remote_cmd(cmds.commitSslCrt, certfile=cert['certfile'])
messages.append(output)
sync[frontend_id]['update'].append({
sync_item['update'].append({
'cert': cert['certfile'],
'messages': messages
})
sync['update_count'] += 1
sync_item['update_count'] += 1
# add to crt-list
for cert in diff['add']:
messages = []
output = self._execute_remote_cmd(cmds.addToSslCrtList, crt_list=diff['path'], certfile=cert)
messages.append(output)
sync[frontend_id]['add'].append({
sync_item['add'].append({
'cert': cert,
'messages': messages
})
sync['add_count'] += 1
sync_item['add_count'] += 1
# remove from crt-list
for cert in diff['del']:
for cert in diff['remove']:
messages = []
output = self._execute_remote_cmd(cmds.delFromSslCrtList, crt_list=diff['path'], certfile=cert)
output = self._execute_remote_cmd(cmds.delFromSslCrtList, crt_list=diff['path'], certfile=cert['certfile'])
messages.append(output)
certs_to_delete.append(cert.split(":")[0])
sync[frontend_id]['remove'].append({
'cert': cert,
certs_to_delete.append(cert['certfile'].split(":")[0])
sync_item['remove'].append({
'cert': cert['certfile'],
'messages': messages
})
sync['remove_count'] += 1
sync_item['remove_count'] += 1
modified_items = sync_item['update_count'] + sync_item['add_count'] + sync_item['remove_count']
if modified_items:
sync['modified'].append(sync_item)
# delete unused certs operation - haproxy does not allow to delete certs in use
for cert in certs_to_delete:
messages = []
output = self._execute_remote_cmd(cmds.delSslCrt, certfile=cert)
messages.append(output)
sync[frontend_id]['del'].append({
cert_item = {
'cert': cert,
'messages': messages
})
}
sync['del_count'] += 1
sync['deleted'].append(cert_item)
if output_format == 'json':
print(json.dumps(self.diff))
if self.output_format == 'json':
print(json.dumps(sync))
if output_format == 'raw':
if self.output_format == 'raw':
print("## SYNC ##")
for frontend_id, crt_list in sync.items():
for crt_list in sync['modified']:
print(f"CRT-LIST: {crt_list['path']}")
print(f" FRONTEND NAME: {crt_list['frontend_name']}")
print(f" FRONTEND ID: {crt_list['frontend_id']}")
@@ -249,6 +339,7 @@ class Diff(SyncWithTarget):
print(f" UPDATE: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
for cert in crt_list['add']:
print(f" ADD: {cert['cert']}")
for message in cert['messages']:
@@ -259,10 +350,10 @@ class Diff(SyncWithTarget):
for message in cert['messages']:
print(" " + repr(message))
for cert in crt_list['del']:
print(f"\n DEL: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
for cert in sync['deleted']:
print(f"\n DEL: {cert['cert']}")
for message in cert['messages']:
print(" " + repr(message))
print()
def __iter__(self):
@@ -339,12 +430,17 @@ class CertList(SyncWithTarget):
def _calc_diff(self):
""" return needed operations to get remote object in sync """
diff = {
'id': self.frontend_id,
'frontend_name': self.frontend_name,
'frontend_id': self.frontend_id,
'path': self.path,
'add': [],
'del': [],
'update': []
'add_count': 0,
'remove': [],
'remove_count': 0,
'update': [],
'update_count': 0,
'total_count': 0
}
# skip when there is no remote crt list
if self.remote is None:
@@ -352,8 +448,26 @@ class CertList(SyncWithTarget):
# certs to add, delete and update on the remote target
diff['add'] = self.diff_list(self.local, self.remote)
diff['del'] = self.diff_list(self.remote, self.local)
diff['add_count'] = len(diff['add'])
#for certpath in self.diff_list(self.local, self.remote):
# diff['add'].append({
# "certfile": certpath,
# "meta": self._execute_remote_cmd(cmds.showSslCert, certfile=certpath)
# })
#diff['add_count'] = len(diff['add'])
# remove
for certpath in self.diff_list(self.remote, self.local):
diff['remove'].append({
"certfile": certpath,
"meta": self._execute_remote_cmd(cmds.showSslCert, certfile=certpath.split(":")[0])
})
diff['remove_count'] = len(diff['remove'])
diff['update'] = [cert.diff for cert in self.certs if cert.diff]
diff['update_count'] = len(diff['update'])
diff['total_count'] = diff['add_count'] + diff['remove_count'] + diff['update_count']
return diff
@@ -499,7 +613,7 @@ def get_args():
)
parser.add_argument(
'command',
choices=['status', 'diff', 'sync', 'transactions', 'abort'],
choices=['diff', 'actions', 'sync', 'transactions', 'abort'],
nargs='+',
help="Execute one or more operations."
)
@@ -520,10 +634,36 @@ def get_args():
)
parser.add_argument(
'--output',
'-o',
help='Specify output format.',
choices=['json', 'raw'],
choices=['json', 'raw', 'bootgrid'],
default="raw"
)
parser.add_argument(
'--page-rows',
help='Limit output to the specified numbers of rows per page.',
default=None
)
parser.add_argument(
'--page',
help='Output page number.',
default=None
)
parser.add_argument(
'--search',
help='Search for string.',
default=None
)
parser.add_argument(
'--sort-col',
help='Sort output on this column.',
default=None
)
parser.add_argument(
'--sort-dir',
help='Sort output in this direction.',
default=None
)
return parser.parse_args()
@@ -562,16 +702,16 @@ def get_crt_lists_from_config(configfile):
args = get_args()
crt_lists = get_crt_lists_from_config(args.config)
diff = Diff(crt_lists=crt_lists)
diff = Diff(crt_lists=crt_lists, **vars(args))
""" Sync ssl certs from configfile to HaProxy """
if "status" in args.command:
diff.show_status(args.output)
if "diff" in args.command:
diff.show_diff(args.output)
diff.show_diff()
if "actions" in args.command:
diff.show_actions()
if "abort" in args.command:
diff.abort(args.output)
diff.abort()
if "transactions" in args.command:
diff.show_transactions(args.output)
diff.show_transactions()
if "sync" in args.command:
diff.sync(args.output)
diff.sync()
@@ -78,15 +78,27 @@ parameters: set-server-weight --server-ids %s --value %s
type:script_output
message:change haproxy weight for multiple server
[cert_diff_list]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py
parameters: actions --output bootgrid --page-rows %s --page %s --search %s --sort-col %s --sort-dir %s
type:script_output
message:Show certificate diff list
[cert_diff]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py
parameters: diff --output json --frontends %s
parameters: diff --frontend-ids %s --output raw
type:script_output
message:Show diff between configured ssl certificates and certs from HAProxy memory for multiple frontends
[cert_actions]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py
parameters: actions --frontend-ids %s --output raw
type:script_output
message:Show diff between configured ssl certificates and certs from HAProxy memory for multiple frontends
[cert_sync]
command:/usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py
parameters: sync --frontends %s --output json
parameters: sync --frontend-ids %s --output json
type:script_output
message:Sync ssl certificates into HAProxy memory for multiple frontends