Printer assignment end to end, and a container that keeps a site's files
Assign printers to a MACHINE and let whichever PC controls it install them, with the observed half kept strictly apart from the assigned half: a collector report can never write an assignment row. Adoption is explicit. Adds a singular relationship type so defaultprinter can mean exactly one, client scripts that converge a bay's queues and correct a drifted one in place, and a wave gate so a fleet-wide driver rollout cannot stampede its own share. On the container side: the compose stack never persisted the instance directory, so a rebuild discarded plugins.json and every upload. It is a volume now, the image ships instance/ owned by the app user so the mountpoint is writable, and the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended since 8.0 went end of life. The image also could not build at all: the frontend stage never copied the plugin staging script it runs. Adds a topology migration guide covering IIS to Docker and back.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"""Computers plugin API endpoints."""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask import Blueprint, request, Response, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.api import db, Asset, AssetType, OperatingSystem, Application, AuditLog, Communication, CommunicationType, Setting, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||
|
||||
from ..models import Computer, ComputerType, ComputerInstalledApp, AccessProtocol, ComputerAccess
|
||||
|
||||
from shopdb.api import require_permission, apply_import_timestamps
|
||||
from shopdb.api import require_permission, require_role, apply_import_timestamps
|
||||
|
||||
computers_bp = Blueprint('computers', __name__)
|
||||
|
||||
@@ -1064,3 +1064,112 @@ def dashboard_sharedmachines():
|
||||
|
||||
out.sort(key=lambda r: -r['pccount'])
|
||||
return success_response(out)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Collector client script
|
||||
# =============================================================================
|
||||
|
||||
CLIENT_SCRIPT_NAME = 'Report-AssetToShopDB.ps1'
|
||||
|
||||
|
||||
def _client_script_path():
|
||||
"""The reporter shipped with this plugin, which is the single source."""
|
||||
import os
|
||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
'client', CLIENT_SCRIPT_NAME)
|
||||
|
||||
|
||||
def _setting_value(key):
|
||||
row = Setting.query.filter_by(key=key).first()
|
||||
return ((row.value if row else '') or '').strip()
|
||||
|
||||
|
||||
def _generate_client_script(source: str, baseurl: str, ranges: str,
|
||||
version: str, generatedon: str) -> str:
|
||||
"""Stamp a site's own values into the reporter's parameter defaults.
|
||||
|
||||
ONLY the defaults are substituted, never the body: the file in the repo
|
||||
stays runnable as-is, so there is no second copy to drift. Everything
|
||||
stamped here is overridable at runtime - the parameter still wins, then the
|
||||
registry - because a bay may need to differ from its site.
|
||||
|
||||
The collector key is NOT stamped in. This file lands on every shop-floor PC,
|
||||
and a token in a file on hundreds of bays cannot be rotated quietly; it is
|
||||
read from HKLM:\\SOFTWARE\\GE\\ShopDB CollectorKey, provisioned per
|
||||
ADOPTING-AT-ANOTHER-SITE.md.
|
||||
"""
|
||||
apiurl = baseurl.rstrip('/') + '/api/collector/computers' if baseurl else ''
|
||||
header = (
|
||||
'# GENERATED by ShopDB {version} on {generatedon}\n'
|
||||
'# for {baseurl}\n'
|
||||
'#\n'
|
||||
'# Re-download after upgrading ShopDB: this copy matches that server\'s\n'
|
||||
'# collector contract. Edits here are lost on the next download - change\n'
|
||||
'# the site settings instead, or pass -ApiUrl / -AllowedRanges.\n'
|
||||
'#\n'
|
||||
'# The collector key is deliberately NOT in this file. Provision it as\n'
|
||||
'# HKLM:\\SOFTWARE\\GE\\ShopDB CollectorKey - see the adoption guide.\n'
|
||||
'\n'
|
||||
).format(version=version, generatedon=generatedon,
|
||||
baseurl=baseurl or 'an unconfigured site (set site_base_url)')
|
||||
|
||||
out = source
|
||||
if apiurl:
|
||||
old = "[string]$ApiUrl = ''"
|
||||
assert old in out, 'the reporter no longer declares $ApiUrl as expected'
|
||||
out = out.replace(old, "[string]$ApiUrl = '{0}'".format(apiurl), 1)
|
||||
if ranges:
|
||||
old = "[string]$AllowedRanges = ''"
|
||||
assert old in out, 'the reporter no longer declares $AllowedRanges as expected'
|
||||
out = out.replace(old, "[string]$AllowedRanges = '{0}'".format(ranges), 1)
|
||||
return header + out
|
||||
|
||||
|
||||
@computers_bp.route('/client-script', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_role('admin')
|
||||
def download_client_script():
|
||||
"""The collector reporter, stamped with THIS site's values.
|
||||
|
||||
Admin-only. It carries no secret, but it does state a site's URL and its
|
||||
internal ranges, which is configuration rather than something to hand out.
|
||||
"""
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
path = _client_script_path()
|
||||
if not os.path.isfile(path):
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
'The collector script is not present in this install',
|
||||
http_code=404)
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as handle:
|
||||
source = handle.read()
|
||||
|
||||
# A site that has not set its public URL still gets a usable script: the
|
||||
# browsing origin is the server the admin is talking to right now.
|
||||
baseurl = _setting_value('site_base_url') or request.url_root
|
||||
# From config, not an import: a plugin reaching into core is an ADR-002
|
||||
# violation and the contract test fails the build for it.
|
||||
version = current_app.config.get('VERSION') or 'unknown'
|
||||
generatedon = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d')
|
||||
|
||||
try:
|
||||
body = _generate_client_script(
|
||||
source, baseurl.strip(), _setting_value('computers_routableranges'),
|
||||
version, generatedon)
|
||||
except AssertionError as exc:
|
||||
return error_response(ErrorCodes.INTERNAL_ERROR, str(exc), http_code=500)
|
||||
|
||||
digest = hashlib.sha256(body.encode('utf-8')).hexdigest()
|
||||
return Response(
|
||||
body,
|
||||
mimetype='text/plain; charset=utf-8',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename={0}'.format(CLIENT_SCRIPT_NAME),
|
||||
# Published so a deployment can verify what it fetched, the same way
|
||||
# the installer publishes one.
|
||||
'X-Script-Sha256': digest,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user