Example #1
0
 def __init__(self, juju_state):
     table = Table()
     table.addHeadings([
         Text('Service'),
         Text('Hardware'),
         Text('Hostname'),
         Text('Machine'),
     ])
     for name, service in juju_state['Services'].items():
         s = ServiceWidget(name, service)
         q(s)
         for u in s.Units:
             m = MachineWidget(
                 juju_state['Machines'][u.Machine.get_text()[0]])
             table.addColumns(u.Name.get_text()[0],
                              [u.Name, m.Hardware, m.DNSName, u.Machine])
     super().__init__(body=table.render())
Example #2
0
 def __init__(self, machine_view):
     table = Table()
     table.addHeadings([
         Text('Hostname'),
         Text('CPU'),
         Text('Storage'),
         Text('Memory'),
     ])
     for m in machine_view:
         m = MachineWidget(m)
         table.addColumns(m.hostname, [
             m.hostname,
             m.cpu_count,
             m.storage,
             m.memory
         ])
     super().__init__(body=table.render())
 def __init__(self, juju_state):
     table = Table()
     table.addHeadings([
         Text('Service'),
         Text('Hardware'),
         Text('Hostname'),
         Text('Machine'),
     ])
     for name, service in juju_state['Services'].items():
         s = ServiceWidget(name, service)
         q(s)
         for u in s.Units:
             m = MachineWidget(juju_state['Machines'][u.Machine.get_text()[0]])
             table.addColumns(u.Name.get_text()[0], [
                 u.Name,
                 m.Hardware,
                 m.DNSName,
                 u.Machine
             ])
     super().__init__(body=table.render())
Example #4
0
class DeployStatusView(WidgetWrap):

    def __init__(self, app):
        self.app = app
        self.deployed = {}
        self.unit_w = None
        self.table = Table()
        super().__init__(Padding.center_80(self.table.render()))

    def refresh_nodes(self):
        """Adds services to the view if they don't already exist

        Schedules UI update on main thread to avoid urwid issues with
        changing listbox state during render.
        """
        EventLoop.loop.event_loop._loop.call_soon_threadsafe(
            self._refresh_nodes_on_main_thread)

    def _refresh_nodes_on_main_thread(self):
        status = model_status()
        for name, service in sorted(status['applications'].items()):
            service_w = ServiceWidget(name, service)
            for unit in service_w.Units:
                try:
                    unit_w = self.deployed[unit._name]
                except:
                    self.deployed[unit._name] = unit
                    unit_w = self.deployed[unit._name]
                    self.table.addColumns(
                        unit._name,
                        [
                            ('fixed', 3, getattr(unit_w, 'Icon')),
                            ('fixed', 50, getattr(unit_w, 'Name')),
                            ('fixed', 20, getattr(unit_w, 'AgentStatus'))
                        ]
                    )

                    if not hasattr(unit_w, 'WorkloadInfo'):
                        continue
                    self.table.addColumns(
                        unit._name,
                        [
                            ('fixed', 5, Text("")),
                            Color.info_context(
                                unit_w.WorkloadInfo)
                        ],
                        force=True)
                self.update_ui_state(unit_w, unit._unit)

    def status_icon_state(self, agent_state):
        if agent_state == "maintenance" \
           or agent_state == "allocating" \
           or agent_state == "executing":
            pending_status = [("pending_icon", "\N{CIRCLED BULLET}"),
                              ("pending_icon", "\N{CIRCLED WHITE BULLET}"),
                              ("pending_icon", "\N{FISHEYE}")]
            status = random.choice(pending_status)
        elif agent_state == "waiting":
            status = ("pending_icon", "\N{HOURGLASS}")
        elif agent_state == "idle" \
                or agent_state == "active":
            status = ("success_icon", "\u2713")
        elif agent_state == "blocked":
            status = ("error_icon", "\N{BLACK FLAG}")
        elif agent_state == "unknown":
            status = ("error_icon", "\N{DOWNWARDS BLACK ARROW}")
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            status = ("error_icon", "?")
        return status

    def update_ui_state(self, unit_w, unit):
        """ Updates individual machine information

        Arguments:
        service: current service
        unit_w: UnitInfo widget
        unit: current unit for service
        """
        try:
            unit_w.Machine.set_text(unit.get('machine', '-'))
            unit_w.PublicAddress.set_text(unit['public-address'])
            unit_w.WorkloadInfo.set_text(unit['workload-status']['info'])
            if unit['workload-status']['status'] != 'unknown':
                unit_w.AgentStatus.set_text(unit['workload-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['workload-status']['status']))
            else:
                unit_w.AgentStatus.set_text(unit['agent-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['agent-status']['status']))
        except Exception as e:
            self.app.log.exception(e)
            self.app.ui.show_exception_message(e)
Example #5
0
class ServicesView(WidgetWrap):

    view_columns = [
        ('Icon', "", 2),
        ('Name', "Service", 0),
        ('AgentStatus', "Status", 20),
        ('PublicAddress', "IP", 20),
        ('Machine', "Machine", 20),
    ]

    def __init__(self, app):
        self.app = app
        self.deployed = {}
        self.unit_w = None
        self.table = Table()

        headings = []
        for key, label, width in self.view_columns:
            # If no width assume ('weight', 1, widget)
            if width == 0:
                headings.append(Color.column_header(Text(label)))
            else:
                headings.append(
                    ('fixed', width, Color.column_header(Text(label))))
        self.table.addHeadings(headings)
        super().__init__(self.table.render())

        self.refresh_nodes()

    def refresh_nodes(self):
        """ Adds services to the view if they don't already exist
        """
        status = model_status()
        for name, service in sorted(status['applications'].items()):
            service_w = ServiceWidget(name, service)
            for unit in service_w.Units:
                services_list = []
                try:
                    unit_w = self.deployed[unit._name]
                except:
                    self.deployed[unit._name] = unit
                    unit_w = self.deployed[unit._name]
                    for k, label, width in self.view_columns:
                        if width == 0:
                            services_list.append(getattr(unit_w, k))
                        else:
                            if not hasattr(unit_w, k):
                                continue
                            services_list.append(
                                ('fixed', width, getattr(unit_w, k)))

                    self.table.addColumns(unit._name, services_list)
                    if not hasattr(unit_w, 'WorkloadInfo'):
                        continue
                    self.table.addColumns(
                        unit._name, [('fixed', 5, Text("")),
                                     Color.info_context(unit_w.WorkloadInfo)],
                        force=True)
                self.update_ui_state(unit_w, unit._unit)

    def status_icon_state(self, agent_state):
        if agent_state == "maintenance" \
           or agent_state == "allocating" \
           or agent_state == "executing":
            pending_status = [("pending_icon", "\N{CIRCLED BULLET}"),
                              ("pending_icon", "\N{CIRCLED WHITE BULLET}"),
                              ("pending_icon", "\N{FISHEYE}")]
            status = random.choice(pending_status)
        elif agent_state == "waiting":
            status = ("pending_icon", "\N{HOURGLASS}")
        elif agent_state == "idle" \
                or agent_state == "active":
            status = ("success_icon", "\u2713")
        elif agent_state == "blocked":
            status = ("error_icon", "\N{BLACK FLAG}")
        elif agent_state == "unknown":
            status = ("error_icon", "\N{DOWNWARDS BLACK ARROW}")
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            status = ("error_icon", "?")
        return status

    def update_ui_state(self, unit_w, unit):
        """ Updates individual machine information

        Arguments:
        service: current service
        unit_w: UnitInfo widget
        unit: current unit for service
        """
        try:
            unit_w.Machine.set_text(unit.get('machine', '-'))
            unit_w.PublicAddress.set_text(unit['public-address'])
            unit_w.WorkloadInfo.set_text(unit['workload-status']['info'])
            if unit['workload-status']['status'] != 'unknown':
                unit_w.AgentStatus.set_text(unit['workload-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['workload-status']['status']))
            else:
                unit_w.AgentStatus.set_text(unit['agent-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['agent-status']['status']))
        except Exception as e:
            self.app.log.exception(e)
            self.app.ui.show_exception_message(e)
class ServicesView(WidgetWrap):

    view_columns = [('icon', "", 2), ('display_name', "Service", 0),
                    ('agent_state', "Status", 12),
                    ('public_address', "IP", 12), ('machine', "Machine", 12),
                    ('container', "Container", 12), ('arch', "Arch ", 12),
                    ('cpu_cores', "Cores", 12), ('mem', "Mem ", 12),
                    ('storage', "Storage", 12)]

    def __init__(self, nodes, juju_state, maas_state, config):
        self.deployed = {}
        self.nodes = [] if nodes is None else nodes
        self.juju_state = juju_state
        self.maas_state = maas_state
        self.config = config
        self.unit_w = None
        self.log_cache = None
        self.table = Table()

        headings = []
        for key, label, width in self.view_columns:
            # If no width assume ('weight', 1, widget)
            if width == 0:
                headings.append(Color.column_header(Text(label)))
            else:
                headings.append(
                    ('fixed', width, Color.column_header(Text(label))))
        self.table.addHeadings(headings)
        super().__init__(self.table.render())

        self.refresh_nodes(self.nodes)

    def refresh_nodes(self, nodes):
        """ Adds services to the view if they don't already exist
        """
        for node in nodes:
            services_list = []
            charm_class, service = node
            if len(service.units) > 0:
                for u in sorted(service.units, key=attrgetter('unit_name')):
                    # Refresh any state changes
                    try:
                        unit_w = self.deployed[u.unit_name]
                    except:
                        hwinfo = self._get_hardware_info(u)
                        self.deployed[u.unit_name] = UnitInfoWidget(
                            u, charm_class, hwinfo)
                        unit_w = self.deployed[u.unit_name]
                        for k, label, width in self.view_columns:
                            if width == 0:
                                services_list.append(getattr(unit_w, k))
                            else:
                                services_list.append(
                                    ('fixed', width, getattr(unit_w, k)))

                        self.table.addColumns(u.unit_name, services_list)
                        self.table.addColumns(
                            u.unit_name,
                            [('fixed', 5, Text("")),
                             Color.frame_subheader(unit_w.workload_info)],
                            force=True)
                    self.update_ui_state(charm_class, u, unit_w)

    def status_icon_state(self, charm_class, unit):
        # unit.agent_state may be "pending" despite errors elsewhere,
        # so we check for error_info first.
        # if the agent_state is "error", _detect_errors returns that.
        error_info = self._detect_errors(unit, charm_class)

        if error_info:
            status = ("error_icon", "\N{TETRAGRAM FOR FAILURE}")
        elif unit.agent_state == "pending":
            pending_status = [("pending_icon", "\N{CIRCLED BULLET}"),
                              ("pending_icon", "\N{CIRCLED WHITE BULLET}"),
                              ("pending_icon", "\N{FISHEYE}")]
            status = random.choice(pending_status)
        elif unit.agent_state == "installed":
            status = ("pending_icon", "\N{HOURGLASS}")
        elif unit.agent_state == "started":
            status = ("success_icon", "\u2713")
        elif unit.agent_state == "stopped":
            status = ("error_icon", "\N{BLACK FLAG}")
        elif unit.agent_state == "down":
            status = ("error_icon", "\N{DOWNWARDS BLACK ARROW}")
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            status = ("error_icon", "?")
        return status

    def update_ui_state(self, charm_class, unit, unit_w):
        """ Updates individual machine information
        """
        unit_w.public_address.set_text(unit.public_address)
        unit_w.agent_state.set_text(unit.agent_state)
        unit_w.icon.set_text(self.status_icon_state(charm_class, unit))
        # Special additional status text for these services
        if 'glance-simplestreams-sync' in unit.unit_name:
            status_oneline = get_sync_status().replace("\n", " - ")
            unit_w.workload_info.set_text(status_oneline)

        elif unit.is_horizon and unit.agent_state == "started":
            unit_w.workload_info.set_text(
                "Login: https://{}/horizon "
                "l:{} p:{}".format(unit.public_address, 'ubuntu',
                                   self.config.getopt('openstack_password')))

        elif unit.is_jujugui and unit.agent_state == "started":
            unit_w.workload_info.set_text("Login: https://{}/".format(
                unit.public_address))
        else:
            unit_w.workload_info.set_text(" {} - {}".format(
                unit.extended_agent_state, unit.workload_info))

    def _get_hardware_info(self, unit):
        """Get hardware info from juju or maas

        Returns list of text and formatting tuples
        """
        juju_machine = self.juju_state.machine(unit.machine_id)
        maas_machine = None
        if self.maas_state:
            maas_machine = self.maas_state.machine(juju_machine.instance_id)

        m = juju_machine
        if juju_machine.arch == "N/A":
            if maas_machine:
                m = maas_machine
            else:
                try:
                    return self._get_container_info(unit)
                except:
                    log.exception(
                        "failed to get container info for unit {}.".format(
                            unit))

        hw_info = self._hardware_info_for_machine(m)
        hw_info['machine'] = juju_machine.machine_id
        return hw_info

    def _get_container_info(self, unit):
        """Attempt to get hardware info of host machine for a unit that looks
        like a container.

        """
        base_machine = self.juju_state.base_machine(unit.machine_id)

        if base_machine.arch == "N/A" and self.maas_state is not None:
            m = self.maas_state.machine(base_machine.instance_id)
        else:
            m = base_machine

        # FIXME: Breaks single install status display
        # base_id, container_type, container_id = unit.machine_id.split('/')
        # ctypestr = dict(kvm="VM", lxc="Container")[container_type]

        # rl = ["{} {} (Machine {}".format(ctypestr, container_id,
        #                                  base_id)]
        try:
            container_id = unit.machine_id.split('/')[-1]
        except:
            log.exception("ERROR: base_machine is {} and m is {}, "
                          "and unit.machine_id is {}".format(
                              base_machine, m, unit.machine_id))
            return "?"

        base_id = base_machine.machine_id
        hw_info = self._hardware_info_for_machine(m)
        hw_info['machine'] = base_id
        hw_info['container'] = container_id
        return hw_info

    def _hardware_info_for_machine(self, m):
        return {
            "arch": m.arch,
            "cpu_cores": m.cpu_cores,
            "mem": m.mem,
            "storage": m.storage,
            "container": '-',
            "machine": 0
        }

    def _detect_errors(self, unit, charm_class):
        """Look in multiple places for an error.

        Return error info string if present,
        or None if no error is found
        """
        unit_machine = self.juju_state.machine(unit.machine_id)

        if unit.agent_state == "error":
            return unit.agent_state_info.lstrip()

        err_info = ""

        if unit.agent_state == 'pending' and \
           unit_machine.agent_state is '' and \
           unit_machine.agent_state_info is not None:

            # detect MAAS API errors, returned as 409 conflict:
            if "409" in unit_machine.agent_state_info:
                if charm_class.constraints is not None:
                    err_info = "Found no machines meeting constraints: "
                    err_info += ', '.join([
                        "{}='{}'".format(k, v)
                        for k, v in charm_class.constraints.items()
                    ])
                else:
                    err_info += "No machines available for unit."
            else:
                err_info += unit_machine.agent_state_info
            return err_info
        return None

    def get_log_text(self, unit_name):
        name = '-'.join(unit_name.split('/'))
        cmd = ("sudo grep {unit} /var/log/juju-ubuntu-local/all-machines.log "
               " | tail -n 2")
        cmd = cmd.format(unit=name)
        out = utils.get_command_output(cmd)
        if out['status'] == 0 and len(out['output']) > 0:
            return out['output']
        else:
            return "No log matches for {}".format(name)
Example #7
0
class DeployStatusView(WidgetWrap):

    def __init__(self, app):
        self.app = app
        self.deployed = {}
        self.unit_w = None
        self.table = Table()
        super().__init__(Padding.center_80(self.table.render()))

    def refresh_nodes(self, applications):
        """Adds services to the view if they don't already exist

        Schedules UI update on main thread to avoid urwid issues with
        changing listbox state during render.
        """
        for name, application in sorted(applications.items()):
            # XXX refactor ubuntui to accept libjuju objects directly
            service = {
                'units': {
                    unit.name: {
                        'public-address': unit.public_address,
                        'machine': unit.machine_id,
                        'agent-status': {
                            'status': unit.agent_status,
                            'info': unit.agent_status_message,
                        },
                        'workload-status': {
                            'status': unit.workload_status,
                            'info': unit.workload_status_message,
                        },
                    } for unit in application.units
                }
            }
            service_w = ServiceWidget(application.name, service)
            for unit in service_w.Units:
                try:
                    unit_w = self.deployed[unit._name]
                except:
                    self.deployed[unit._name] = unit
                    unit_w = self.deployed[unit._name]
                    self.table.addColumns(
                        unit._name,
                        [
                            ('fixed', 3, getattr(unit_w, 'Icon')),
                            ('fixed', 50, getattr(unit_w, 'Name')),
                            ('fixed', 20, getattr(unit_w, 'AgentStatus'))
                        ]
                    )

                    if not hasattr(unit_w, 'WorkloadInfo'):
                        continue
                    self.table.addColumns(
                        unit._name,
                        [
                            ('fixed', 5, Text("")),
                            Color.info_context(
                                unit_w.WorkloadInfo)
                        ],
                        force=True)
                self.update_ui_state(unit_w, unit._unit)

    def status_icon_state(self, agent_state):
        if agent_state == "maintenance" \
           or agent_state == "allocating" \
           or agent_state == "executing":
            pending_status = [("pending_icon", "\N{CIRCLED BULLET}"),
                              ("pending_icon", "\N{CIRCLED WHITE BULLET}"),
                              ("pending_icon", "\N{FISHEYE}")]
            status = random.choice(pending_status)
        elif agent_state == "waiting":
            status = ("pending_icon", "\N{HOURGLASS}")
        elif agent_state == "idle" \
                or agent_state == "active":
            status = ("success_icon", "\u2713")
        elif agent_state == "blocked":
            status = ("error_icon", "\N{BLACK FLAG}")
        elif agent_state == "unknown":
            status = ("error_icon", "\N{DOWNWARDS BLACK ARROW}")
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            status = ("error_icon", "?")
        return status

    def update_ui_state(self, unit_w, unit):
        """ Updates individual machine information

        Arguments:
        service: current service
        unit_w: UnitInfo widget
        unit: current unit for service
        """
        try:
            unit_w.Machine.set_text(unit.get('machine', '-'))
            unit_w.PublicAddress.set_text(unit['public-address'])
            unit_w.WorkloadInfo.set_text(unit['workload-status']['info'])
            if unit['workload-status']['status'] != 'unknown':
                unit_w.AgentStatus.set_text(unit['workload-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['workload-status']['status']))
            else:
                unit_w.AgentStatus.set_text(unit['agent-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['agent-status']['status']))
        except Exception:
            raise
Example #8
0
class ServicesView(WidgetWrap):

    view_columns = [
        ('Icon', "", 2),
        ('Name', "Service", 0),
        ('AgentStatus', "Status", 20),
        ('PublicAddress', "IP", 20),
        ('Machine', "Machine", 20),
    ]

    def __init__(self, app):
        self.app = app
        self.deployed = {}
        self.unit_w = None
        self.table = Table()

        headings = []
        for key, label, width in self.view_columns:
            # If no width assume ('weight', 1, widget)
            if width == 0:
                headings.append(Color.column_header(Text(label)))
            else:
                headings.append(
                    ('fixed', width, Color.column_header(Text(label))))
        self.table.addHeadings(headings)
        super().__init__(self.table.render())

        self.refresh_nodes()

    def refresh_nodes(self):
        """ Adds services to the view if they don't already exist
        """
        status = model_status()
        for name, service in sorted(status['applications'].items()):
            service_w = ServiceWidget(name, service)
            for unit in service_w.Units:
                services_list = []
                try:
                    unit_w = self.deployed[unit._name]
                except:
                    self.deployed[unit._name] = unit
                    unit_w = self.deployed[unit._name]
                    for k, label, width in self.view_columns:
                        if width == 0:
                            services_list.append(getattr(unit_w, k))
                        else:
                            if not hasattr(unit_w, k):
                                continue
                            services_list.append(('fixed', width,
                                                  getattr(unit_w, k)))

                    self.table.addColumns(unit._name, services_list)
                    if not hasattr(unit_w, 'WorkloadInfo'):
                        continue
                    self.table.addColumns(
                        unit._name,
                        [
                            ('fixed', 5, Text("")),
                            Color.info_context(
                                unit_w.WorkloadInfo)
                        ],
                        force=True)
                self.update_ui_state(unit_w, unit._unit)

    def status_icon_state(self, agent_state):
        if agent_state == "maintenance" \
           or agent_state == "allocating" \
           or agent_state == "executing":
            pending_status = [("pending_icon", "\N{CIRCLED BULLET}"),
                              ("pending_icon", "\N{CIRCLED WHITE BULLET}"),
                              ("pending_icon", "\N{FISHEYE}")]
            status = random.choice(pending_status)
        elif agent_state == "waiting":
            status = ("pending_icon", "\N{HOURGLASS}")
        elif agent_state == "idle" \
                or agent_state == "active":
            status = ("success_icon", "\u2713")
        elif agent_state == "blocked":
            status = ("error_icon", "\N{BLACK FLAG}")
        elif agent_state == "unknown":
            status = ("error_icon", "\N{DOWNWARDS BLACK ARROW}")
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            status = ("error_icon", "?")
        return status

    def update_ui_state(self, unit_w, unit):
        """ Updates individual machine information

        Arguments:
        service: current service
        unit_w: UnitInfo widget
        unit: current unit for service
        """
        try:
            unit_w.Machine.set_text(unit.get('machine', '-'))
            unit_w.PublicAddress.set_text(unit['public-address'])
            unit_w.WorkloadInfo.set_text(unit['workload-status']['info'])
            if unit['workload-status']['status'] != 'unknown':
                unit_w.AgentStatus.set_text(unit['workload-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['workload-status']['status']))
            else:
                unit_w.AgentStatus.set_text(unit['agent-status']['status'])
                unit_w.Icon.set_text(
                    self.status_icon_state(unit['agent-status']['status']))
        except Exception as e:
            self.app.log.exception(e)
            self.app.ui.show_exception_message(e)
Example #9
0
class StepsView(WidgetWrap):

    INPUT_TYPES = {
        'text': StringEditor(),
        'password': PasswordEditor(),
        'boolean': YesNo(),
        'integer': IntegerEditor()
    }

    def __init__(self, app, steps, cb=None):
        """ init

        Arguments:
        cb: process step callback
        """
        self.app = app
        self.table = Table()
        self.cb = cb

        self.steps_queue = steps
        for step_model in self.steps_queue:
            step_widget = self.add_step_widget(step_model)
            self.table.addColumns(
                step_model.path,
                [
                    ('fixed', 3, step_widget['icon']),
                    step_widget['description'],
                ]
            )
            # Need to still prompt for the user to submit
            # even though no questions are asked
            if len(step_widget['additional_input']) == 0:
                self.table.addRow(
                    Padding.right_20(
                        Color.button_primary(
                            submit_btn(on_press=self.submit,
                                       user_data=(step_model, step_widget)),
                            focus_map='button_primary focus')), False)
            for i in step_widget['additional_input']:
                self.table.addRow(Padding.line_break(""), False)
                self.table.addColumns(
                    step_model.path,
                    [
                        ('weight', 0.5, Padding.left(i['label'], left=5)),
                        ('weight', 1, Color.string_input(
                            i['input'],
                            focus_map='string_input focus')),
                    ], force=True
                )
                self.table.addRow(
                    Padding.right_20(
                        Color.button_primary(
                            submit_btn(
                                on_press=self.submit,
                                user_data=(step_model, step_widget)),
                            focus_map='button_primary focus')), False)
                self.table.addRow(Padding.line_break(""), False)

        self.table.addRow(Padding.center_20(
                        Color.button_primary(
                            done_btn(on_press=self.done, label="View Summary"),
                            focus_map='button_primary focus')))
        super().__init__(Padding.center_80(self.table.render()))

    def add_step_widget(self, step_model):
        if not step_model.viewable:
            self.app.log.debug("{} is not viewable, skipping".format(
                step_model))
            return

        step_widget_dict = {'title': Text(step_model.title),
                            'description': Text(step_model.description),
                            'result': Text(step_model.result),
                            'icon': Text(step_model.icon)}
        step_widget_dict['additional_input'] = []
        if len(step_model.additional_input) > 0:
            for i in step_model.additional_input:
                widget = {
                    "label": Text(i['label']),
                    "key": i['key'],
                    "input": self.INPUT_TYPES.get(i['type'])
                }
                if 'default' in i:
                    widget['input'] = StringEditor(default=i['default'])

                step_widget_dict['additional_input'].append(widget)
        return step_widget_dict

    def done(self, *args):
        self.cb({}, done=True)

    def submit(self, btn, stepmodel_stepwidget):
        step_model, step_widget = stepmodel_stepwidget

        # set icon
        step_model.icon = step_widget['icon']
        # merge the step_widget input data into our step model
        for i in step_model.additional_input:
            try:
                matching_widget = [x for x in step_widget['additional_input']
                                   if x['key'] == i['key']][0]
                i['input'] = matching_widget['input'].value
            except IndexError as e:
                self.app.log.error(
                    "Tried to pull a value from an "
                    "invalid input: {}/{}".format(e,
                                                  matching_widget))
        self.cb(step_model)

    def update_icon_state(self, icon, result_code):
        """ updates status icon

        Arguments:
        icon: icon widget
        result_code: 3 types of results, error, waiting, complete
        """
        if result_code == "error":
            icon.set_text(
                ("error_icon", "\N{BLACK FLAG}"))
        elif result_code == "waiting":
            icon.set_text(("pending_icon", "\N{HOURGLASS}"))
        elif result_code == "active":
            icon.set_text(("success_icon", "\N{BALLOT BOX WITH CHECK}"))
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            icon.set_text(("error_icon", "?"))
class ServicesView(WidgetWrap):

    view_columns = [
        ('icon', "", 2),
        ('display_name', "Service", 0),
        ('agent_state', "Status", 12),
        ('public_address', "IP", 12),
        ('machine', "Machine", 12),
        ('container', "Container", 12),
        ('arch', "Arch ", 12),
        ('cpu_cores', "Cores", 12),
        ('mem', "Mem ", 12),
        ('storage', "Storage", 12)
    ]

    def __init__(self, nodes, juju_state, maas_state, config):
        self.deployed = {}
        self.nodes = [] if nodes is None else nodes
        self.juju_state = juju_state
        self.maas_state = maas_state
        self.config = config
        self.unit_w = None
        self.log_cache = None
        self.table = Table()

        headings = []
        for key, label, width in self.view_columns:
            # If no width assume ('weight', 1, widget)
            if width == 0:
                headings.append(Color.column_header(Text(label)))
            else:
                headings.append(
                    ('fixed', width, Color.column_header(Text(label))))
        self.table.addHeadings(headings)
        super().__init__(self.table.render())

        self.refresh_nodes(self.nodes)

    def refresh_nodes(self, nodes):
        """ Adds services to the view if they don't already exist
        """
        for node in nodes:
            services_list = []
            charm_class, service = node
            if len(service.units) > 0:
                for u in sorted(service.units, key=attrgetter('unit_name')):
                    # Refresh any state changes
                    try:
                        unit_w = self.deployed[u.unit_name]
                    except:
                        hwinfo = self._get_hardware_info(u)
                        self.deployed[u.unit_name] = UnitInfoWidget(
                            u,
                            charm_class,
                            hwinfo)
                        unit_w = self.deployed[u.unit_name]
                        for k, label, width in self.view_columns:
                            if width == 0:
                                services_list.append(getattr(unit_w, k))
                            else:
                                services_list.append(('fixed', width,
                                                      getattr(unit_w, k)))

                        self.table.addColumns(u.unit_name, services_list)
                        self.table.addColumns(
                            u.unit_name,
                            [
                                ('fixed', 5, Text("")),
                                Color.frame_subheader(unit_w.workload_info)
                            ],
                            force=True)
                    self.update_ui_state(charm_class, u,
                                         unit_w)

    def status_icon_state(self, charm_class, unit):
        # unit.agent_state may be "pending" despite errors elsewhere,
        # so we check for error_info first.
        # if the agent_state is "error", _detect_errors returns that.
        error_info = self._detect_errors(unit, charm_class)

        if error_info:
            status = ("error_icon", "\N{TETRAGRAM FOR FAILURE}")
        elif unit.agent_state == "pending":
            pending_status = [("pending_icon", "\N{CIRCLED BULLET}"),
                              ("pending_icon", "\N{CIRCLED WHITE BULLET}"),
                              ("pending_icon", "\N{FISHEYE}")]
            status = random.choice(pending_status)
        elif unit.agent_state == "installed":
            status = ("pending_icon", "\N{HOURGLASS}")
        elif unit.agent_state == "started":
            status = ("success_icon", "\u2713")
        elif unit.agent_state == "stopped":
            status = ("error_icon", "\N{BLACK FLAG}")
        elif unit.agent_state == "down":
            status = ("error_icon", "\N{DOWNWARDS BLACK ARROW}")
        else:
            # NOTE: Should not get here, if we do make sure we account
            # for that error type above.
            status = ("error_icon", "?")
        return status

    def update_ui_state(self, charm_class, unit, unit_w):
        """ Updates individual machine information
        """
        unit_w.public_address.set_text(unit.public_address)
        unit_w.agent_state.set_text(unit.agent_state)
        unit_w.icon.set_text(self.status_icon_state(charm_class, unit))
        # Special additional status text for these services
        if 'glance-simplestreams-sync' in unit.unit_name:
            status_oneline = get_sync_status().replace("\n", " - ")
            unit_w.workload_info.set_text(status_oneline)

        elif unit.is_horizon and unit.agent_state == "started":
            unit_w.workload_info.set_text(
                "Login: https://{}/horizon "
                "l:{} p:{}".format(
                    unit.public_address,
                    'ubuntu',
                    self.config.getopt('openstack_password')))

        elif unit.is_jujugui and unit.agent_state == "started":
            unit_w.workload_info.set_text(
                "Login: https://{}/".format(
                    unit.public_address))
        else:
            unit_w.workload_info.set_text(
                " {} - {}".format(unit.extended_agent_state,
                                  unit.workload_info))

    def _get_hardware_info(self, unit):
        """Get hardware info from juju or maas

        Returns list of text and formatting tuples
        """
        juju_machine = self.juju_state.machine(unit.machine_id)
        maas_machine = None
        if self.maas_state:
            maas_machine = self.maas_state.machine(juju_machine.instance_id)

        m = juju_machine
        if juju_machine.arch == "N/A":
            if maas_machine:
                m = maas_machine
            else:
                try:
                    return self._get_container_info(unit)
                except:
                    log.exception(
                        "failed to get container info for unit {}.".format(
                            unit))

        hw_info = self._hardware_info_for_machine(m)
        hw_info['machine'] = juju_machine.machine_id
        return hw_info

    def _get_container_info(self, unit):
        """Attempt to get hardware info of host machine for a unit that looks
        like a container.

        """
        base_machine = self.juju_state.base_machine(unit.machine_id)

        if base_machine.arch == "N/A" and self.maas_state is not None:
            m = self.maas_state.machine(base_machine.instance_id)
        else:
            m = base_machine

        # FIXME: Breaks single install status display
        # base_id, container_type, container_id = unit.machine_id.split('/')
        # ctypestr = dict(kvm="VM", lxc="Container")[container_type]

        # rl = ["{} {} (Machine {}".format(ctypestr, container_id,
        #                                  base_id)]
        try:
            container_id = unit.machine_id.split('/')[-1]
        except:
            log.exception("ERROR: base_machine is {} and m is {}, "
                          "and unit.machine_id is {}".format(
                              base_machine, m, unit.machine_id))
            return "?"

        base_id = base_machine.machine_id
        hw_info = self._hardware_info_for_machine(m)
        hw_info['machine'] = base_id
        hw_info['container'] = container_id
        return hw_info

    def _hardware_info_for_machine(self, m):
        return {"arch": m.arch,
                "cpu_cores": m.cpu_cores,
                "mem": m.mem,
                "storage": m.storage,
                "container": '-',
                "machine": 0}

    def _detect_errors(self, unit, charm_class):
        """Look in multiple places for an error.

        Return error info string if present,
        or None if no error is found
        """
        unit_machine = self.juju_state.machine(unit.machine_id)

        if unit.agent_state == "error":
            return unit.agent_state_info.lstrip()

        err_info = ""

        if unit.agent_state == 'pending' and \
           unit_machine.agent_state is '' and \
           unit_machine.agent_state_info is not None:

            # detect MAAS API errors, returned as 409 conflict:
            if "409" in unit_machine.agent_state_info:
                if charm_class.constraints is not None:
                    err_info = "Found no machines meeting constraints: "
                    err_info += ', '.join(["{}='{}'".format(k, v) for k, v
                                           in charm_class.constraints.items()])
                else:
                    err_info += "No machines available for unit."
            else:
                err_info += unit_machine.agent_state_info
            return err_info
        return None

    def get_log_text(self, unit_name):
        name = '-'.join(unit_name.split('/'))
        cmd = ("sudo grep {unit} /var/log/juju-ubuntu-local/all-machines.log "
               " | tail -n 2")
        cmd = cmd.format(unit=name)
        out = utils.get_command_output(cmd)
        if out['status'] == 0 and len(out['output']) > 0:
            return out['output']
        else:
            return "No log matches for {}".format(name)