Exemple #1
0
    def render(self, what, row, tags, custom_vars):
        if what == "service" and row["service_cached_at"]:
            output = _(
                "This service is based on cached agent data and cannot be rescheduled."
            )
            output += " %s" % render_cache_info(what, row)

            return "cannot_reschedule", output, None

        # Reschedule button
        if row[what + "_check_type"] == 2:
            return  # shadow hosts/services cannot be rescheduled

        if (row[what + "_active_checks_enabled"] == 1
            or row[what + '_check_command'].startswith('check_mk-')) \
            and config.user.may('action.reschedule'):

            servicedesc = ''
            wait_svc = ''
            icon = 'reload'
            txt = _('Reschedule check')

            if what == 'service':
                servicedesc = row['service_description'].replace("\\", "\\\\")
                wait_svc = servicedesc

                # Use Check_MK service for cmk based services
                if row[what + '_check_command'].startswith('check_mk-'):
                    servicedesc = 'Check_MK'
                    icon = 'reload_cmk'
                    txt = _('Reschedule \'Check_MK\' service')

            url = 'onclick:cmk.views.reschedule_check(this, \'%s\', \'%s\', \'%s\', \'%s\');' % \
                (row["site"], row["host_name"], html.urlencode(servicedesc), html.urlencode(wait_svc))
            return icon, txt, url
Exemple #2
0
 def _add_wato_folder_to_url(self, url):
     if not self._wato_folder:
         return url
     if '/' in url:
         return url  # do not append wato_folder to non-Check_MK-urls
     if '?' in url:
         return url + "&wato_folder=" + html.urlencode(self._wato_folder)
     return url + "?wato_folder=" + html.urlencode(self._wato_folder)
Exemple #3
0
    def page(self):
        config.user.need_permission("general.see_crash_reports")

        filename = "Checkmk_Crash_%s_%s_%s.tar.gz" % \
            (html.urlencode(self._site_id), html.urlencode(self._crash_id), time.strftime("%Y-%m-%d_%H-%M-%S"))

        html.response.headers['Content-Disposition'] = 'Attachment; filename=%s' % filename
        html.response.headers['Content-Type'] = 'application/x-tar'
        html.write_binary(_pack_crash_report(self._get_serialized_crash_report()))
Exemple #4
0
def page_download_crash_report():
    site = html.request.var("site")
    host = html.request.var("host")
    service = html.request.var("service")

    filename = "Check_MK_Crash_%s_%s_%s.tar.gz" % \
        (html.urlencode(host), html.urlencode(service), time.strftime("%Y-%m-%d_%H-%M-%S"))

    tardata = get_crash_report_archive_as_string(site, host, service)
    html.response.headers['Content-Disposition'] = 'Attachment; filename=%s' % filename
    html.response.headers['Content-Type'] = 'application/x-tar'
    html.write(tardata)
Exemple #5
0
def host_service_graph_popup_pnp(site, host_name, service_description):
    pnp_host = cmk.utils.pnp_cleanup(host_name)
    pnp_svc = cmk.utils.pnp_cleanup(service_description)
    url_prefix = config.site(site)["url_prefix"]

    if html.mobile:
        url = url_prefix + ("pnp4nagios/index.php?kohana_uri=/mobile/popup/%s/%s" % \
            (html.urlencode(pnp_host), html.urlencode(pnp_svc)))
    else:
        url = url_prefix + ("pnp4nagios/index.php/popup?host=%s&srv=%s" % \
            (html.urlencode(pnp_host), html.urlencode(pnp_svc)))

    html.write(url)
Exemple #6
0
def host_service_graph_dashlet_pnp(graph_identification):
    site = graph_identification[1]["site"]
    source = int(graph_identification[1]["graph_index"])

    pnp_host = cmk.utils.pnp_cleanup(graph_identification[1]["host_name"])
    pnp_svc = cmk.utils.pnp_cleanup(
        graph_identification[1]["service_description"])
    url_prefix = config.site(site)["url_prefix"]

    pnp_theme = html.get_theme()
    if pnp_theme == "classic":
        pnp_theme = "multisite"

    html.write(url_prefix + "pnp4nagios/index.php/image?host=%s&srv=%s&source=%d&view=%s&theme=%s" % \
        (html.urlencode(pnp_host), html.urlencode(pnp_svc), source, html.request.var("timerange"), pnp_theme))
Exemple #7
0
 def show(self):
     group_type = self._group_type_ident()
     html.open_ul()
     for name, alias in sites.all_groups(group_type.replace("group", "")):
         url = "view.py?view_name=%s&%s=%s" % (group_type, group_type, html.urlencode(name))
         bulletlink(alias or name, url)
     html.close_ul()
Exemple #8
0
def _handle_not_authenticated() -> Response:
    if fail_silently():
        # While api call don't show the login dialog
        raise MKUnauthenticatedException(_('You are not authenticated.'))

    # Redirect to the login-dialog with the current url as original target
    # Never render the login form directly when accessing urls like "index.py"
    # or "dashboard.py". This results in strange problems.
    if html.myfile != 'login':
        post_login_url = makeuri(request, [])
        if html.myfile != "index":
            # Ensure that users start with a navigation after they have logged in
            post_login_url = makeuri_contextless(
                request, [("start_url", post_login_url)], filename="index.py")
        raise HTTPRedirect(
            '%scheck_mk/login.py?_origtarget=%s' %
            (config.url_prefix(), html.urlencode(post_login_url)))

    # This either displays the login page or validates the information submitted
    # to the login form. After successful login a http redirect to the originally
    # requested page is performed.
    login_page = login.LoginPage()
    login_page.set_no_html_output(plain_error())
    login_page.handle_page()

    return html.response
Exemple #9
0
def _check_auth_cookie(cookie_name: str) -> Optional[UserId]:
    username, issue_time, cookie_hash = _parse_auth_cookie(cookie_name)
    _check_parsed_auth_cookie(username, issue_time, cookie_hash)

    # Check whether or not there is an idle timeout configured, delete cookie and
    # require the user to renew the log when the timeout exceeded.
    if userdb.login_timed_out(username, issue_time):
        del_auth_cookie()
        return None

    # Check whether or not a single user session is allowed at a time and the user
    # is doing this request with the currently active session.
    if config.single_user_session is not None:
        session_id = _get_session_id_from_cookie(username)
        if not userdb.is_valid_user_session(username, session_id):
            del_auth_cookie()
            return None

    # Once reached this the cookie is a good one. Renew it!
    _renew_cookie(cookie_name, username)

    if html.myfile != 'user_change_pw':
        result = userdb.need_to_change_pw(username)
        if result:
            raise HTTPRedirect('user_change_pw.py?_origtarget=%s&reason=%s' %
                               (html.urlencode(html.makeuri([])), result))

    # Return the authenticated username
    return username
Exemple #10
0
    def _do_login(self) -> None:
        """handle the sent login form"""
        if not html.request.var('_login'):
            return

        try:
            if not config.user_login:
                raise MKUserError(None,
                                  _('Login is not allowed on this site.'))

            username_var = html.request.get_unicode_input('_username', '')
            assert username_var is not None
            username = UserId(username_var.rstrip())
            if not username:
                raise MKUserError('_username', _('No username given.'))

            password = html.request.var('_password', '')
            if not password:
                raise MKUserError('_password', _('No password given.'))

            default_origtarget = config.url_prefix() + "check_mk/"
            origtarget = html.get_url_input("_origtarget", default_origtarget)

            # Disallow redirections to:
            #  - logout.py: Happens after login
            #  - side.py: Happens when invalid login is detected during sidebar refresh
            if "logout.py" in origtarget or 'side.py' in origtarget:
                origtarget = default_origtarget

            result = userdb.check_credentials(username, password)
            if result:
                # use the username provided by the successful login function, this function
                # might have transformed the username provided by the user. e.g. switched
                # from mixed case to lower case.
                username = result

                session_id = userdb.on_succeeded_login(username)

                # The login succeeded! Now:
                # a) Set the auth cookie
                # b) Unset the login vars in further processing
                # c) Redirect to really requested page
                _create_auth_session(username, session_id)

                # Never use inplace redirect handling anymore as used in the past. This results
                # in some unexpected situations. We simpy use 302 redirects now. So we have a
                # clear situation.
                # userdb.need_to_change_pw returns either False or the reason description why the
                # password needs to be changed
                change_pw_result = userdb.need_to_change_pw(username)
                if change_pw_result:
                    raise HTTPRedirect(
                        'user_change_pw.py?_origtarget=%s&reason=%s' %
                        (html.urlencode(origtarget), change_pw_result))
                raise HTTPRedirect(origtarget)

            userdb.on_failed_login(username)
            raise MKUserError(None, _('Invalid credentials.'))
        except MKUserError as e:
            html.add_user_error(e.varname, e)
Exemple #11
0
 def show(self) -> None:
     html.open_ul()
     for group in bi.get_aggregation_group_trees():
         bulletlink(
             group, "view.py?view_name=aggr_group&aggr_group=%s" %
             html.urlencode(group))
     html.close_ul()
Exemple #12
0
    def _wato_link(self, folder, site, hostname, where):
        if not config.wato_enabled:
            return None

        if display_options.enabled(display_options.X):
            url = "wato.py?folder=%s&host=%s" % \
              (html.urlencode(folder), html.urlencode(hostname))
            if where == "inventory":
                url += "&mode=inventory"
                help_txt = _("Edit services")
                icon = "services"
            else:
                url += "&mode=edit_host"
                help_txt = _("Edit this host")
                icon = "wato"
            return icon, help_txt, url

        return None
Exemple #13
0
    def show_without_timeseries(self):
        @site_query
        def query(cls, properties, context):
            return [
                "host_name", "service_check_command", "service_description", "service_perf_data",
                "service_state"
            ]

        col_names, data = query(  # pylint: disable=unbalanced-tuple-unpacking
            self, json.dumps(self.vs_parameters().value_to_json(self._dashlet_spec)),
            self._dashlet_spec["context"])

        if not data:
            raise MKUserError(None, _("There are no metrics meeting your context filters."))

        row = dict(zip(col_names, data[0]))

        site = row["site"]
        host = row["host_name"]
        service = row["service_description"]
        metric = self._dashlet_spec.get("metric", "")

        t_metrics = translate_perf_data(row["service_perf_data"], row["service_check_command"])
        chosen_metric = t_metrics.get(metric)
        if chosen_metric is None:
            raise MKUserError(
                None,
                _("The configured metric \"%s\" could not be found. For the "
                  "selected service \"%s\" you can choose from the following metrics: %s") %
                (metric, service, ", ".join([m["title"] for m in t_metrics.values()])))

        svc_url = "view.py?view_name=service&site=%s&host=%s&service=%s" % (
            html.urlencode(site), html.urlencode(host), html.urlencode(service))
        links = {
            "site": html.render_a(site,
                                  "view.py?view_name=sitehosts&site=%s" % (html.urlencode(site))),
            "host": html.render_a(
                host, "view.py?view_name=host&site=%s&host=%s" %
                (html.urlencode(site), html.urlencode(host))),
            "service": html.render_a(service, svc_url)
        }
        render_options = self._dashlet_spec["render_options"]

        svc_state = row["service_state"]

        html.open_div(class_="metric")
        metric_spec = {
            "site": site,
            "host": host,
            "service": service,
            "metric": chosen_metric.get("title", metric)
        }
        titles = self._get_titles(metric_spec, links, render_options)
        self._render_metric_content(chosen_metric, render_options, titles, svc_state, svc_url)
        html.close_div()
    def show(self):
        host = self._dashlet_spec['context'].get("host", html.request.var("host"))
        if not host:
            raise MKUserError('host', _('Missing needed host parameter.'))

        service = self._dashlet_spec['context'].get("service")
        if not service:
            service = "_HOST_"

        metric = self._dashlet_spec["metric"] if "metric" in self._dashlet_spec else ""
        site = self._get_site_by_host_name(host)
        metric_spec = {"site": site, "host": host, "service": service, "metric": metric}
        svc_url = "view.py?view_name=service&site=%s&host=%s&service=%s" % (
            html.urlencode(site), html.urlencode(host), html.urlencode(service))
        links = {
            "site": html.render_a(site,
                                  "view.py?view_name=sitehosts&site=%s" % (html.urlencode(site))),
            "host": html.render_a(
                host, "view.py?view_name=host&site=%s&host=%s" %
                (html.urlencode(site), html.urlencode(host))),
            "service": html.render_a(service, svc_url)
        }
        render_options = self._dashlet_spec["metric_render_options"]
        metrics = self._query_for_metrics_of_host(site, host, service)
        t_metrics = self._get_translated_metrics_from_perf_data(metrics)
        chosen_metric_name, metric_choices = self._get_chosen_metric(t_metrics, metric)
        svc_state = metrics["svc_state"]

        html.open_div(class_="metric")
        if metrics:
            if chosen_metric_name:
                chosen_metric = t_metrics[chosen_metric_name]
                titles = self._get_titles(metric_spec, links, render_options)
                self._render_metric_content(chosen_metric, render_options, titles, svc_state,
                                            svc_url)
            else:
                html.open_div(class_="no_metric_match")
                if metric_choices:
                    # TODO: Fix this handling of no available/matching metric
                    # after the implementation of host/site contexts
                    warning_txt = HTML(
                        _("The given metric \"%s\" could not be found.\
                            For the selected service \"%s\" you can choose from the following metrics:"
                          % (metric, service)))
                    warning_txt += html.render_ul("".join(
                        [str(html.render_li(b)) for _a, b in metric_choices]))
                    html.show_warning(warning_txt)
                else:
                    html.show_warning(_("No metric could be found."))
                html.close_div()
        else:
            html.show_warning(_("There are no metrics meeting your context filters."))
        html.close_div()
Exemple #15
0
def _handle_not_authenticated():
    if _fail_silently():
        # While api call don't show the login dialog
        raise MKUnauthenticatedException(_('You are not authenticated.'))

    # Redirect to the login-dialog with the current url as original target
    # Never render the login form directly when accessing urls like "index.py"
    # or "dashboard.py". This results in strange problems.
    if html.myfile != 'login':
        raise HTTPRedirect(
            '%scheck_mk/login.py?_origtarget=%s' %
            (config.url_prefix(), html.urlencode(html.makeuri([]))))
    # This either displays the login page or validates the information submitted
    # to the login form. After successful login a http redirect to the originally
    # requested page is performed.
    login_page = login.LoginPage()
    login_page.set_no_html_output(_plain_error())
    login_page.handle_page()
Exemple #16
0
    def render(self, what, row, tags, custom_vars):
        # service_check_command looks like:
        # u"check_mk_active-bi_aggr!... '-b' 'http://localhost/$HOSTNAME$' ... '-a' 'Host foobar' ..."
        if what == "service" and row.get("service_check_command",
                                         "").startswith("check_mk_active-bi_aggr!"):
            args = row['service_check_command']
            start = args.find('-b\' \'') + 5
            end = args.find('\' ', start)
            base_url = args[start:end].rstrip('/')
            base_url = base_url.replace('$HOSTADDRESS$', row['host_address'])
            base_url = base_url.replace('$HOSTNAME$', row['host_name'])

            start = args.find('-a\' \'') + 5
            end = args.find('\' ', start)
            aggr_name = args[start:end]

            url = "%s/check_mk/view.py?view_name=aggr_single&aggr_name=%s" % \
                  (base_url, html.urlencode(aggr_name))

            return 'aggr', _('Open this Aggregation'), url
Exemple #17
0
def _check_auth_cookie(cookie_name: str) -> Optional[UserId]:
    username, session_id, cookie_hash = _parse_auth_cookie(cookie_name)
    _check_parsed_auth_cookie(username, session_id, cookie_hash)

    try:
        userdb.on_access(username, session_id)
    except MKAuthException:
        del_auth_cookie()
        raise

    # Once reached this the cookie is a good one. Renew it!
    _renew_cookie(cookie_name, username, session_id)

    if html.myfile != 'user_change_pw':
        result = userdb.need_to_change_pw(username)
        if result:
            raise HTTPRedirect('user_change_pw.py?_origtarget=%s&reason=%s' %
                               (html.urlencode(html.makeuri([])), result))

    # Return the authenticated username
    return username
def test_htmllib_integration(register_builtin_html):
    assert type(html.encoder) == htmllib.Encoder

    assert html.urlencode_vars([]) == ""
    assert html.urlencode("") == ""
Exemple #19
0
def do_login():
    # handle the sent login form
    if html.request.var('_login'):
        try:
            username = html.get_unicode_input('_username', '').rstrip()
            if username == '':
                raise MKUserError('_username', _('No username given.'))

            password = html.request.var('_password', '')
            if password == '':
                raise MKUserError('_password', _('No password given.'))

            default_origtarget = config.url_prefix() + "check_mk/"
            origtarget = html.get_url_input("_origtarget", default_origtarget)

            # Disallow redirections to:
            #  - logout.py: Happens after login
            #  - side.py: Happens when invalid login is detected during sidebar refresh
            if "logout.py" in origtarget or 'side.py' in origtarget:
                origtarget = default_origtarget

            # None        -> User unknown, means continue with other connectors
            # '<user_id>' -> success
            # False       -> failed
            result = userdb.hook_login(username, password)
            if result:
                # use the username provided by the successful login function, this function
                # might have transformed the username provided by the user. e.g. switched
                # from mixed case to lower case.
                username = result

                # When single user session mode is enabled, check that there is not another
                # active session
                userdb.ensure_user_can_init_session(username)

                # reset failed login counts
                userdb.on_succeeded_login(username)

                # The login succeeded! Now:
                # a) Set the auth cookie
                # b) Unset the login vars in further processing
                # c) Redirect to really requested page
                create_auth_session(username)

                # Never use inplace redirect handling anymore as used in the past. This results
                # in some unexpected situations. We simpy use 302 redirects now. So we have a
                # clear situation.
                # userdb.need_to_change_pw returns either False or the reason description why the
                # password needs to be changed
                result = userdb.need_to_change_pw(username)
                if result:
                    raise HTTPRedirect('user_change_pw.py?_origtarget=%s&reason=%s' %
                                       (html.urlencode(origtarget), result))
                else:
                    raise HTTPRedirect(origtarget)
            else:
                userdb.on_failed_login(username)
                raise MKUserError(None, _('Invalid credentials.'))
        except MKUserError as e:
            html.add_user_error(e.varname, e)
            return "%s" % e
def test_htmllib_integration(register_builtin_html):
    assert isinstance(html.encoder, URLEncoder)

    assert html.urlencode_vars([]) == ""
    assert html.urlencode("") == ""
Exemple #21
0
    def show(self):
        mode = self._host_mode_ident()
        sites.live().set_prepend_site(True)
        query = "GET hosts\nColumns: name state worst_service_state\nLimit: 100\n"
        view = "host"

        if mode == "problems":
            view = "problemsofhost"
            # Exclude hosts and services in downtime
            svc_query = "GET services\nColumns: host_name\n"\
                        "Filter: state > 0\nFilter: scheduled_downtime_depth = 0\n"\
                        "Filter: host_scheduled_downtime_depth = 0\nAnd: 3"
            problem_hosts = {x[1] for x in sites.live().query(svc_query)}

            query += "Filter: state > 0\nFilter: scheduled_downtime_depth = 0\nAnd: 2\n"
            for host in problem_hosts:
                query += "Filter: name = %s\n" % host
            query += "Or: %d\n" % (len(problem_hosts) + 1)

        hosts = sites.live().query(query)
        sites.live().set_prepend_site(False)
        hosts.sort()

        longestname = 0
        for site, host, state, worstsvc in hosts:
            longestname = max(longestname, len(host))
        if longestname > 15:
            num_columns = 1
        else:
            num_columns = 2

        target = views.get_context_link(config.user.id, view)
        html.open_table(class_="allhosts")
        col = 1
        for site, host, state, worstsvc in hosts:
            if col == 1:
                html.open_tr()
            html.open_td()

            if state > 0 or worstsvc == 2:
                statecolor = 2
            elif worstsvc == 1:
                statecolor = 1
            elif worstsvc == 3:
                statecolor = 3
            else:
                statecolor = 0
            html.open_div(class_=["statebullet", "state%d" % statecolor])
            html.nbsp()
            html.close_div()
            link(host, target + "&host=%s&site=%s" % (html.urlencode(host), html.urlencode(site)))
            html.close_td()
            if col == num_columns:
                html.close_tr()
                col = 1
            else:
                col += 1

        if col < num_columns:
            html.close_tr()
        html.close_table()
Exemple #22
0
    def show(self):
        hosts = self._get_hosts()
        num_hosts = len(hosts)

        if num_hosts > 900:
            html.write_text(
                _("Sorry, I will not display more than 900 hosts."))
            return

        # Choose smallest square number large enough
        # to show all hosts
        n = 1
        while n * n < num_hosts:
            n += 1

        rows = int(num_hosts / n)
        lastcols = num_hosts % n
        if lastcols > 0:
            rows += 1

        # Calculate cell size (Automatic sizing with 100% does not work here)
        # - Get cell spacing: 1px between each cell
        # - Substract the cell spacing for each column from the total width
        # - Then divide the total width through the number of columns
        # - Then get the full-digit width of the cell and summarize the rest
        #   to be substracted from the cell width
        # This is not a 100% solution but way better than having no links
        cell_spacing = 1
        cell_size = int((snapin_width - cell_spacing * (n + 1)) / n)
        cell_size, cell_size_rest = divmod(cell_size, 1)
        style = 'width:%spx' % (snapin_width - n * cell_size_rest)

        html.open_table(class_=["content_center", "hostmatrix"],
                        cellspacing="0",
                        style=["border-collapse:collapse;", style])
        col = 1
        row = 1
        for site, host, state, has_been_checked, worstsvc, downtimedepth in sorted(
                hosts):
            if col == 1:
                html.open_tr()
            if downtimedepth > 0:
                s = "d"
            elif not has_been_checked:
                s = "p"
            elif worstsvc == 2 or state == 1:
                s = "2"
            elif worstsvc == 3 or state == 2:
                s = "3"
            elif worstsvc == 1:
                s = "1"
            else:
                s = "0"
            url = "view.py?view_name=host&site=%s&host=%s" % (
                html.urlencode(site), html.urlencode(host))
            html.open_td(class_=["state", "state%s" % s])
            html.a(
                '',
                href=url,
                title=host,
                target="main",
                style=["width:%spx;" % cell_size,
                       "height:%spx;" % cell_size])
            html.close_td()

            if col == n or (row == rows and n == lastcols):
                html.open_tr()
                col = 1
                row += 1
            else:
                col += 1
        html.close_table()
Exemple #23
0
 def clone_url(self):
     backurl = html.urlencode(html.makeuri([]))
     return "edit_%s.py?load_user=%s&load_name=%s&mode=clone&back=%s" \
                 % (self.type_name(), self.owner(), self.name(), backurl)
Exemple #24
0
 def add_url(cls):
     return 'create_view_dashlet.py?name=%s&back=%s' % \
         (html.urlencode(html.request.var('name')), html.urlencode(html.makeuri([('edit', '1')])))
Exemple #25
0
    def show(self, width, context):
        hosts = self._get_livestatus(context)
        num_hosts = len(hosts)

        if num_hosts > 900:
            html.write_text(_("Sorry, I will not display more than 900 hosts."))
            return

        # Choose smallest square number large enough
        # to show all hosts
        n = 1
        while n * n < num_hosts:
            n += 1

        rows = int(num_hosts / n)
        lastcols = num_hosts % n
        if lastcols > 0:
            rows += 1

        # Calculate cell size (Automatic sizing with 100% does not work here)
        # This is not a 100% solution but way better than having no links
        cell_spacing = 3
        cell_size = (width - cell_spacing * n) / n
        cell_height = 2 * cell_size / 3

        # Add one cell_spacing so that the cells fill the whole snapin width.
        # The spacing of the last cell overflows on the right.
        html.open_table(class_=["hostmatrix"], style=['width:%spx' % (width + cell_spacing)])
        col, row = 1, 1
        for site, host, state, has_been_checked, worstsvc, downtimedepth in sorted(hosts):
            if col == 1:
                html.open_tr()

            if downtimedepth > 0:
                s = "d"
            elif not has_been_checked:
                s = "p"
            elif worstsvc == 2 or state == 1:
                s = "2"
            elif worstsvc == 3 or state == 2:
                s = "3"
            elif worstsvc == 1:
                s = "1"
            else:
                s = "0"
            url = "view.py?view_name=host&site=%s&host=%s" % (html.urlencode(site),
                                                              html.urlencode(host))
            html.open_td(style=[
                "width:%.2fpx" % (cell_size + cell_spacing),
                "height:%.2fpx" % (cell_height + cell_spacing)
            ])
            html.a('',
                   href=url,
                   title=host,
                   target="main",
                   class_=["state", "state%s" % s],
                   style=["width:%.2fpx;" % cell_size,
                          "height:%.2fpx;" % cell_height])
            html.close_td()

            if col == n or (row == rows and n == lastcols):
                html.open_tr()
                col = 1
                row += 1
            else:
                col += 1
        html.close_table()