| #!/usr/bin/python |
| |
| """Tracks and exports conman status information for e.g. ledmonitor.""" |
| |
| # This may seem over-engineered, but conman has enough loosely-coupled moving |
| # parts that it is worth being able to reason formally and separately about the |
| # state of the system. Otherwise it would be very easy for new conman code to |
| # create subtle bugs in e.g. LED behavior. |
| |
| import inspect |
| import logging |
| import os |
| |
| |
| class P(object): |
| """Enumerate propositions about conman status. |
| |
| Using class attributes rather than just strings will help prevent typos. |
| """ |
| |
| TRYING_OPEN = 'TRYING_OPEN' |
| TRYING_WLAN = 'TRYING_WLAN' |
| CONNECTED_TO_OPEN = 'CONNECTED_TO_OPEN' |
| CONNECTED_TO_WLAN = 'CONNECTED_TO_WLAN' |
| HAVE_CONFIG = 'HAVE_CONFIG' |
| HAVE_WORKING_CONFIG = 'HAVE_WORKING_CONFIG' |
| CAN_REACH_ACS = 'CAN_REACH_ACS' |
| # Were we able to connect to the ACS last time we expected to be able to? |
| COULD_REACH_ACS = 'COULD_REACH_ACS' |
| CAN_REACH_INTERNET = 'CAN_REACH_INTERNET' |
| PROVISIONING_FAILED = 'PROVISIONING_FAILED' |
| |
| |
| # Format: { proposition: (implications, counter-implications), ... } |
| # If you want to add a new proposition to the Status class, just edit this dict. |
| IMPLICATIONS = { |
| P.TRYING_OPEN: ( |
| (), |
| (P.CONNECTED_TO_OPEN, P.TRYING_WLAN, P.CONNECTED_TO_WLAN) |
| ), |
| P.TRYING_WLAN: ( |
| (), |
| (P.TRYING_OPEN, P.CONNECTED_TO_OPEN, P.CONNECTED_TO_WLAN) |
| ), |
| P.CONNECTED_TO_OPEN: ( |
| (), |
| (P.CONNECTED_TO_WLAN, P.TRYING_OPEN, P.TRYING_WLAN) |
| ), |
| P.CONNECTED_TO_WLAN: ( |
| (P.HAVE_WORKING_CONFIG,), |
| (P.CONNECTED_TO_OPEN, P.TRYING_OPEN, P.TRYING_WLAN) |
| ), |
| P.CAN_REACH_ACS: ( |
| (P.COULD_REACH_ACS,), |
| (P.TRYING_OPEN, P.TRYING_WLAN) |
| ), |
| P.COULD_REACH_ACS: ( |
| (), |
| (P.PROVISIONING_FAILED,), |
| ), |
| P.PROVISIONING_FAILED: ( |
| (), |
| (P.COULD_REACH_ACS,), |
| ), |
| P.HAVE_WORKING_CONFIG: ( |
| (), |
| (P.HAVE_CONFIG,), |
| ), |
| } |
| |
| |
| class Proposition(object): |
| """Represents a proposition. |
| |
| May imply truth or falsity of other propositions. |
| """ |
| |
| def __init__(self, name, export_path): |
| self._name = name |
| self._export_path = export_path |
| self._value = False |
| self._implications = set() |
| self._counter_implications = set() |
| self._impliers = set() |
| self._counter_impliers = set() |
| |
| def implies(self, implication): |
| self._counter_implications.discard(implication) |
| self._implications.add(implication) |
| # pylint: disable=protected-access |
| implication._implied_by(self) |
| |
| def implies_not(self, counter_implication): |
| self._implications.discard(counter_implication) |
| self._counter_implications.add(counter_implication) |
| # pylint: disable=protected-access |
| counter_implication._counter_implied_by(self) |
| |
| def _implied_by(self, implier): |
| self._counter_impliers.discard(implier) |
| self._impliers.add(implier) |
| |
| def _counter_implied_by(self, counter_implier): |
| self._impliers.discard(counter_implier) |
| self._counter_impliers.add(counter_implier) |
| |
| def set(self, value): |
| if value == self._value: |
| return |
| |
| self._value = value |
| self.export() |
| logging.debug('%s is now %s', self._name, self._value) |
| |
| if value: |
| for implication in self._implications: |
| implication.set(True) |
| for counter_implication in self._counter_implications: |
| counter_implication.set(False) |
| # Contrapositive: (A -> ~B) -> (B -> ~A) |
| for counter_implier in self._counter_impliers: |
| counter_implier.set(False) |
| # Contrapositive: (A -> B) -> (~B -> ~A) |
| else: |
| for implier in self._impliers: |
| implier.set(False) |
| |
| def export(self): |
| filepath = os.path.join(self._export_path, self._name) |
| if self._value: |
| if not os.path.exists(filepath): |
| open(filepath, 'w') |
| else: |
| if os.path.exists(filepath): |
| os.unlink(filepath) |
| |
| |
| class Status(object): |
| """Provides a convenient API for conman to describe system status.""" |
| |
| def __init__(self, export_path): |
| if not os.path.isdir(export_path): |
| os.makedirs(export_path) |
| |
| self._export_path = export_path |
| |
| self._propositions = { |
| p: Proposition(p, self._export_path) |
| for p in dict(inspect.getmembers(P)) if not p.startswith('_') |
| } |
| |
| for p, (implications, counter_implications) in IMPLICATIONS.iteritems(): |
| for implication in implications: |
| self._propositions[p].implies(self._propositions[implication]) |
| for counter_implication in counter_implications: |
| self._propositions[p].implies_not( |
| self._propositions[counter_implication]) |
| |
| def _proposition(self, p): |
| return self._propositions[p] |
| |
| def __setattr__(self, attr, value): |
| """Allow setting of propositions with attributes. |
| |
| If _propositions contains an attribute 'FOO', then `Status().foo = True` |
| will set that Proposition to True. This means that this class doesn't have |
| to be changed when IMPLICATIONS is updated. |
| |
| Args: |
| attr: The attribute name. |
| value: The attribute value. |
| """ |
| if hasattr(self, '_propositions') and not hasattr(self, attr): |
| if attr.islower(): |
| if attr.upper() in self._propositions: |
| self._propositions[attr.upper()].set(value) |
| return |
| |
| super(Status, self).__setattr__(attr, value) |