2020-03-24 10:15:07 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# Copyright 2020 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
|
|
|
|
import attr
|
|
|
|
import jsonschema
|
|
|
|
|
|
|
|
log = logging.getLogger("console_conf.models.systems")
|
|
|
|
|
|
|
|
# This json schema describes the recovery systems data. Allow additional
|
|
|
|
# properties at each level so that console-conf does not have to be exactly in
|
|
|
|
# sync with snapd.
|
|
|
|
_RECOVERY_SYSTEMS_SCHEMA = {
|
|
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
|
|
"title": "systems",
|
|
|
|
"type": "object",
|
|
|
|
"additionalProperties": True,
|
|
|
|
"required": ["systems"],
|
|
|
|
"properties": {
|
|
|
|
"systems": {
|
|
|
|
"type": "array",
|
|
|
|
"items": {
|
|
|
|
"type": "object",
|
|
|
|
"required": ["label", "brand", "model"],
|
|
|
|
"properties": {
|
|
|
|
"label": {"type": "string"},
|
|
|
|
"actions": {
|
|
|
|
"type": "array",
|
|
|
|
"items": {
|
|
|
|
"type": "object",
|
|
|
|
"additionalProperties": True,
|
|
|
|
"required": ["title", "mode"],
|
|
|
|
"properties": {
|
|
|
|
"title": {"type": "string"},
|
|
|
|
"mode": {"type": "string"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"brand": {
|
|
|
|
"type": "object",
|
|
|
|
"additionalProperties": True,
|
|
|
|
"required": ["id", "username", "display-name"],
|
|
|
|
"properties": {
|
|
|
|
"id": {"type": "string"},
|
|
|
|
"username": {"type": "string"},
|
|
|
|
"display-name": {"type": "string"},
|
|
|
|
"validation": {"type": "string"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"model": {
|
|
|
|
"type": "object",
|
|
|
|
"additionalProperties": True,
|
|
|
|
"required": ["model", "brand-id", "display-name"],
|
|
|
|
"properties": {
|
|
|
|
"model": {"type": "string"},
|
|
|
|
"brand-id": {"type": "string"},
|
|
|
|
"display-name": {"type": "string"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class RecoverySystemsModel:
|
|
|
|
"""Recovery chooser data"""
|
|
|
|
|
|
|
|
def __init__(self, systems_data):
|
|
|
|
self.systems = systems_data
|
|
|
|
# current selection
|
|
|
|
self._selection = None
|
2020-04-01 13:58:09 +00:00
|
|
|
self._current = None
|
|
|
|
# find which system is current one, but be robust if none is marked as
|
|
|
|
# such
|
|
|
|
cs = [s for s in systems_data if s.current]
|
|
|
|
if cs:
|
|
|
|
self._current = cs[0]
|
2020-03-24 10:15:07 +00:00
|
|
|
|
|
|
|
def select(self, system, action):
|
|
|
|
self._selection = SelectedSystemAction(system=system, action=action)
|
|
|
|
|
2020-04-07 15:22:29 +00:00
|
|
|
def unselect(self):
|
|
|
|
self._selection = None
|
|
|
|
|
2020-03-24 10:15:07 +00:00
|
|
|
@property
|
|
|
|
def selection(self):
|
|
|
|
return self._selection
|
|
|
|
|
2020-04-01 13:58:09 +00:00
|
|
|
@property
|
|
|
|
def current(self):
|
|
|
|
return self._current
|
2020-03-24 10:15:07 +00:00
|
|
|
|
2020-04-01 13:58:09 +00:00
|
|
|
@staticmethod
|
|
|
|
def from_systems(recovery_systems):
|
2020-03-24 10:15:07 +00:00
|
|
|
systems = []
|
2020-04-01 13:58:09 +00:00
|
|
|
for syst in recovery_systems:
|
|
|
|
m = syst["model"]
|
|
|
|
b = syst["brand"]
|
2020-03-24 10:15:07 +00:00
|
|
|
model = SystemModel(
|
|
|
|
model=m["model"], brand_id=m["brand-id"], display_name=m["display-name"]
|
|
|
|
)
|
|
|
|
brand = Brand(
|
|
|
|
ID=b["id"],
|
|
|
|
username=b["username"],
|
|
|
|
display_name=b["display-name"],
|
|
|
|
validation=b.get("validation", "unproven"),
|
|
|
|
)
|
|
|
|
actions = []
|
2020-04-01 13:58:09 +00:00
|
|
|
for a in syst.get("actions", []):
|
2020-03-24 10:15:07 +00:00
|
|
|
actions.append(SystemAction(title=a["title"], mode=a["mode"]))
|
|
|
|
s = RecoverySystem(
|
2020-04-01 13:58:09 +00:00
|
|
|
current=syst.get("current", False),
|
|
|
|
label=syst["label"],
|
2020-03-24 10:15:07 +00:00
|
|
|
model=model,
|
|
|
|
brand=brand,
|
|
|
|
actions=actions,
|
|
|
|
)
|
|
|
|
systems.append(s)
|
|
|
|
|
|
|
|
return RecoverySystemsModel(systems)
|
|
|
|
|
2020-04-01 13:58:09 +00:00
|
|
|
@staticmethod
|
|
|
|
def from_systems_stream(chooser_input):
|
|
|
|
"""Deserialize recovery systems from input JSON stream."""
|
|
|
|
try:
|
|
|
|
dec = json.load(chooser_input)
|
|
|
|
jsonschema.validate(dec, _RECOVERY_SYSTEMS_SCHEMA)
|
|
|
|
systems = dec.get("systems", [])
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
log.exception("cannot decode recovery systems info")
|
|
|
|
raise
|
|
|
|
except jsonschema.ValidationError:
|
|
|
|
log.exception("cannot validate recovery systems data")
|
|
|
|
raise
|
|
|
|
|
|
|
|
return RecoverySystemsModel.from_systems(systems)
|
|
|
|
|
2020-03-24 10:15:07 +00:00
|
|
|
@staticmethod
|
|
|
|
def to_response_stream(obj, chooser_output):
|
|
|
|
"""Serialize an object with selected action as JSON to the given output
|
|
|
|
stream.
|
|
|
|
"""
|
|
|
|
if not isinstance(obj, SelectedSystemAction):
|
|
|
|
raise TypeError("unexpected type: {}".format(type(obj)))
|
|
|
|
|
|
|
|
choice = {
|
|
|
|
"label": obj.system.label,
|
2020-04-01 13:58:09 +00:00
|
|
|
"action": {
|
|
|
|
"mode": obj.action.mode,
|
|
|
|
"title": obj.action.title,
|
|
|
|
},
|
2020-03-24 10:15:07 +00:00
|
|
|
}
|
|
|
|
return json.dump(choice, fp=chooser_output)
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s
|
|
|
|
class RecoverySystem:
|
|
|
|
current = attr.ib()
|
|
|
|
label = attr.ib()
|
|
|
|
model = attr.ib()
|
|
|
|
brand = attr.ib()
|
|
|
|
actions = attr.ib()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return (
|
|
|
|
self.current == other.current
|
|
|
|
and self.label == other.label
|
|
|
|
and self.model == other.model
|
|
|
|
and self.brand == other.brand
|
|
|
|
and self.actions == other.actions
|
2023-07-25 21:26:25 +00:00
|
|
|
)
|
2020-03-24 10:15:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@attr.s
|
|
|
|
class Brand:
|
|
|
|
ID = attr.ib()
|
|
|
|
username = attr.ib()
|
|
|
|
display_name = attr.ib()
|
|
|
|
validation = attr.ib()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return (
|
|
|
|
self.ID == other.ID
|
|
|
|
and self.username == other.username
|
|
|
|
and self.display_name == other.display_name
|
|
|
|
and self.validation == other.validation
|
2023-07-25 21:26:25 +00:00
|
|
|
)
|
2020-03-24 10:15:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@attr.s
|
|
|
|
class SystemModel:
|
|
|
|
model = attr.ib()
|
|
|
|
brand_id = attr.ib()
|
|
|
|
display_name = attr.ib()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return (
|
|
|
|
self.model == other.model
|
|
|
|
and self.brand_id == other.brand_id
|
|
|
|
and self.display_name == other.display_name
|
2023-07-25 21:26:25 +00:00
|
|
|
)
|
2020-03-24 10:15:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@attr.s
|
|
|
|
class SystemAction:
|
|
|
|
title = attr.ib()
|
|
|
|
mode = attr.ib()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.title == other.title and self.mode == other.mode
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s
|
|
|
|
class SelectedSystemAction:
|
|
|
|
system = attr.ib()
|
|
|
|
action = attr.ib()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.system == other.system and self.action == other.action
|