]> git.ipfire.org Git - pbs.git/commitdiff
mirrors: Refactor everything
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 16 May 2023 13:49:19 +0000 (13:49 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 16 May 2023 13:50:25 +0000 (13:50 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
16 files changed:
Makefile.am
src/buildservice/mirrors.py
src/crontab/pakfire-build-service
src/database.sql
src/scripts/pakfire-build-service
src/templates/mirrors/delete.html
src/templates/mirrors/detail.html [deleted file]
src/templates/mirrors/edit.html
src/templates/mirrors/index.html [new file with mode: 0644]
src/templates/mirrors/list.html [deleted file]
src/templates/mirrors/modules/list.html [new file with mode: 0644]
src/templates/mirrors/new.html [deleted file]
src/templates/mirrors/show.html [new file with mode: 0644]
src/web/__init__.py
src/web/mirrors.py
src/web/repos.py

index b13acdf815348c4d1c0e936f64ffaaaf9d30863b..72c4e2501ef61dd7121f94f76f3db2ef8b66f045 100644 (file)
@@ -268,13 +268,17 @@ templates_messagesdir = $(templatesdir)/messages
 
 dist_templates_mirrors_DATA = \
        src/templates/mirrors/delete.html \
-       src/templates/mirrors/detail.html \
        src/templates/mirrors/edit.html \
-       src/templates/mirrors/list.html \
-       src/templates/mirrors/new.html
+       src/templates/mirrors/index.html \
+       src/templates/mirrors/show.html
 
 templates_mirrorsdir = $(templatesdir)/mirrors
 
+dist_templates_mirrors_modules_DATA = \
+       src/templates/mirrors/modules/list.html
+
+templates_mirrors_modulesdir = $(templates_mirrorsdir)/modules
+
 dist_templates_modules_DATA = \
        src/templates/modules/commits-table.html \
        src/templates/modules/commit-message.html \
index feeea0dc7248279a5b8703fb8bd556dd07677f4a..2c0bcdf7210b87503ff908458058673202bb669b 100644 (file)
@@ -1,26 +1,24 @@
 #!/usr/bin/python
 
+import asyncio
 import datetime
 import logging
-import math
+import random
 import socket
-import time
-import tornado.httpclient
+import tornado.netutil
 import urllib.parse
 
 import location
 
 from . import base
+from . import httpclient
 
-from .decorators import lazy_property
+from .decorators import *
 
 # Setup logging
 log = logging.getLogger("pbs.mirrors")
 
 class Mirrors(base.Object):
-       def init(self):
-               self.location = location.Database("/var/lib/location/database.db")
-
        def _get_mirror(self, query, *args):
                res = self.db.get(query, *args)
 
@@ -34,99 +32,145 @@ class Mirrors(base.Object):
                        yield Mirror(self.backend, row.id, data=row)
 
        def __iter__(self):
-               mirrors = self._get_mirrors("SELECT * FROM mirrors \
-                       WHERE deleted IS FALSE ORDER BY hostname")
+               mirrors = self._get_mirrors("""
+                       SELECT
+                               *
+                       FROM
+                               mirrors
+                       WHERE
+                               deleted_at IS NULL
+                       ORDER BY
+                               hostname
+                       """,
+               )
 
                return iter(mirrors)
 
-       @property
-       def random(self):
+       async def create(self, hostname, path, owner, contact, notes, user=None, check=True):
                """
-                       Returns all mirrors in a random order
+                       Creates a new mirror
                """
-               return self._get_mirrors("SELECT * FROM mirrors \
-                       WHERE deleted IS FALSE ORDER BY RANDOM()")
+               mirror = self._get_mirror("""
+                       INSERT INTO
+                               mirrors
+                       (
+                               hostname,
+                               path,
+                               owner,
+                               contact,
+                               notes,
+                               created_by
+                       )
+                       VALUES(
+                               %s, %s, %s, %s, %s, %s
+                       )
+                       RETURNING
+                               *
+                       """, hostname, path, owner, contact, notes, user,
+               )
 
-       def create(self, hostname, path="", owner=None, contact=None, user=None):
-               mirror = self._get_mirror("INSERT INTO mirrors(hostname, path, owner, contact) \
-                       VALUES(%s, %s, %s, %s) RETURNING *", hostname, path, owner, contact)
+               log.info("Mirror %s has been created" % mirror)
+
+               # Perform the first check
+               if check:
+                       await mirror.check(force=True)
 
                return mirror
 
        def get_by_id(self, id):
-               return self._get_mirror("SELECT * FROM mirrors WHERE id = %s", id)
+               return self._get_mirror("""
+                       SELECT
+                               *
+                       FROM
+                               mirrors
+                       WHERE
+                               id = %s
+                       """, id,
+               )
 
        def get_by_hostname(self, hostname):
-               return self._get_mirror("SELECT * FROM mirrors \
-                       WHERE hostname = %s AND deleted IS FALSE", hostname)
-
-       def make_mirrorlist(self, client_address=None):
-               network = self.location.lookup(client_address)
-
-               # Walk through all mirrors in a random order
-               # and put all preferred mirrors to the front of the list
-               # and everything else at the end
-               mirrors = []
-               for mirror in self.random:
-                       if mirror.is_preferred_for_country(network.country_code if network else None):
-                               mirrors.insert(0, mirror)
-                       else:
-                               mirrors.append(mirror)
+               return self._get_mirror("""
+                       SELECT
+                               *
+                       FROM
+                               mirrors
+                       WHERE
+                               deleted_at IS NULL
+                       AND
+                               hostname = %s
+                       """, hostname,
+               )
+
+       def get_mirrors_for_address(self, address):
+               """
+                       Returns all mirrors in random order with preferred mirrors first
+               """
+               # Lookup the client
+               network = self.location.lookup(address)
+
+               def __sort(mirror):
+                       # Generate some random value for each mirror
+                       r = random.random()
+
+                       # Put preferred mirrors first
+                       if network and mirror.is_preferred_for_network(network):
+                               r += 1
+
+                       return r
 
-               return mirrors
+               # Fetch all mirrors and shuffle them, but put preferred mirrors first
+               return sorted(self, key=__sort)
 
-       def check(self, **kwargs):
+       @lazy_property
+       def location(self):
+               """
+                       The location database
+               """
+               return location.Database("/var/lib/location/database.db")
+
+       @lazy_property
+       def resolver(self):
+               """
+                       A DNS resolver
+               """
+               return tornado.netutil.ThreadedResolver()
+
+       async def check(self, *args, **kwargs):
                """
                        Runs the mirror check for all mirrors
                """
-               for mirror in self:
-                       with self.db.transaction():
-                               mirror.check(**kwargs)
+               # Check all mirrors concurrently
+               async with asyncio.TaskGroup() as tg:
+                       for mirror in self:
+                               tg.create_task(mirror.check(*args, **kwargs))
 
 
 class Mirror(base.DataObject):
        table = "mirrors"
 
+       def __str__(self):
+               return self.hostname
+
        def __lt__(self, other):
                if isinstance(other, self.__class__):
                        return self.hostname < other.hostname
 
                return NotImplemented
 
-       def set_hostname(self, hostname):
-               self._set_attribute("hostname", hostname)
-
-       hostname = property(lambda self: self.data.hostname, set_hostname)
-
-       def set_deleted(self, deleted):
-               self._set_attribute("deleted", deleted)
-
-       deleted = property(lambda s: s.data.deleted, set_deleted)
-
-       def has_perm(self, user):
-               # Anonymous users have no permission
-               if not user:
-                       return False
-
-               # Admins have all permissions
-               return user.is_admin()
+       @property
+       def hostname(self):
+               return self.data.hostname
 
        @property
        def path(self):
                return self.data.path
 
-       def set_path(self, path):
-               self._set_attribute("path", path)
-
-       path = property(lambda self: self.data.path, set_path)
-
        @property
        def url(self):
                return self.make_url()
 
        def make_url(self, path=""):
-               url = "%s://%s%s" % (
-                       "https" if self.supports_https else "http",
+               url = "https://%s%s" % (
                        self.hostname,
                        self.path
                )
@@ -136,111 +180,301 @@ class Mirror(base.DataObject):
 
                return urllib.parse.urljoin(url, path)
 
-       def set_supports_https(self, supports_https):
-               self._set_attribute("supports_https", supports_https)
+       @property
+       def last_check_success(self):
+               """
+                       True if the last check was successful
+               """
+               return self.data.last_check_success
+
+       @property
+       def last_check_at(self):
+               """
+                       The timestamp of the last check
+               """
+               return self.data.last_check_at
+
+       @property
+       def error(self):
+               """
+                       The error message of the last unsuccessful check
+               """
+               return self.data.error
+
+       @property
+       def created_at(self):
+               return self.data.created_at
+
+       # Delete
+
+       def delete(self, user):
+               """
+                       Deleted this mirror
+               """
+               self._set_attribute_now("deleted_at")
+               if user:
+                       self._set_attribute("deleted_by", user)
+
+               # Log the event
+               log.info("Mirror %s has been deleted" % self)
+
+       def has_perm(self, user):
+               # Anonymous users have no permission
+               if not user:
+                       return False
+
+               # Admins have all permissions
+               return user.is_admin()
+
+       # Owner
 
-       supports_https = property(lambda s: s.data.supports_https, set_supports_https)
+       def get_owner(self):
+               return self.data.owner
 
        def set_owner(self, owner):
                self._set_attribute("owner", owner)
 
-       owner = property(lambda self: self.data.owner or "", set_owner)
+       owner = property(get_owner, set_owner)
+
+       # Contact
+
+       def get_contact(self):
+               return self.data.contact
 
        def set_contact(self, contact):
                self._set_attribute("contact", contact)
 
-       contact = property(lambda self: self.data.contact or "", set_contact)
+       contact = property(get_contact, set_contact)
 
-       def check(self, connect_timeout=10, request_timeout=10):
-               log.info("Running mirror check for %s" % self.hostname)
+       # Notes
 
-               client = tornado.httpclient.HTTPClient()
+       def get_notes(self):
+               return self.data.notes
 
-               # Get URL for .timestamp
-               url = self.make_url(".timestamp")
-               log.debug("  Fetching %s..." % url)
+       def set_notes(self, notes):
+               self._set_attribute("notes", notes or "")
 
-               # Record start time
-               time_start = time.time()
+       notes = property(get_notes, set_notes)
 
-               http_status = None
-               last_sync_at = None
-               status = "OK"
+       # Country Code & ASN
 
-               # XXX needs to catch connection resets, DNS errors, etc.
+       @property
+       def country_code(self):
+               """
+                       The country code
+               """
+               return self.data.country_code
+
+       @lazy_property
+       def asn(self):
+               """
+                       The Autonomous System
+               """
+               if self.data.asn:
+                       return self.backend.mirrors.location.get_as(self.data.asn)
 
+       async def _update_country_code_and_asn(self):
+               """
+                       Updates the country code of this mirror
+               """
+               # Resolve the hostname
                try:
-                       response = client.fetch(url,
-                               connect_timeout=connect_timeout,
-                               request_timeout=request_timeout)
+                       addresses = await self.backend.mirrors.resolver.resolve(self.hostname, port=443)
 
-                       # We expect the response to be an integer
-                       # which holds the timestamp of the last sync
-                       # in seconds since epoch UTC
-                       try:
-                               timestamp = int(response.body)
+               # XXX Catch this!
+               except socket.gaierror as e:
+                       # Name or service not known
+                       if e.errno == -2:
+                               log.error("Could not resolve %s: %s" % (self, e))
+                               return
 
-                       # If we could not parse the timestamp, we probably got
-                       # an error page or something similar.
-                       # So that's an error then...
-                       except ValueError:
-                               status = "ERROR"
+                       # Raise anything else
+                       raise e
 
-                       # Timestamp seems to be okay
-                       else:
-                               # Convert to datetime
-                               last_sync_at = datetime.datetime.utcfromtimestamp(timestamp)
+               for family, address in addresses:
+                       # Extract the address
+                       address = address[0]
 
-                               # Must have synced within 24 hours
-                               now = datetime.datetime.utcnow()
-                               if now - last_sync_at >= datetime.timedelta(hours=24):
-                                       status = "OUTOFSYNC"
+                       # Lookup the address
+                       network = self.backend.mirrors.location.lookup(address)
+                       if not network or not network.country_code:
+                               continue
 
-               except tornado.httpclient.HTTPError as e:
-                       http_status = e.code
-                       status = "ERROR"
+                       # Store the country code
+                       self._set_attribute("country_code", network.country_code)
 
-               finally:
-                       response_time = time.time() - time_start
+                       # Store the ASN
+                       self._set_attribute("asn", network.asn)
 
-               # Log check
-               self.db.execute("INSERT INTO mirrors_checks(mirror_id, response_time, \
-                       http_status, last_sync_at, status) VALUES(%s, %s, %s, %s, %s)",
-                       self.id, response_time, http_status, last_sync_at, status)
+                       # Once is enough
+                       break
 
-       @lazy_property
-       def last_check(self):
-               res = self.db.get("SELECT * FROM mirrors_checks \
-                       WHERE mirror_id = %s ORDER BY timestamp DESC LIMIT 1", self.id)
+       def is_preferred_for_network(self, network):
+               """
+                       Returns True if this mirror is preferred for clients on the given network.
+               """
+               # If the AS matches, we will prefer this
+               if self.asn and self.asn.number == network.asn:
+                       return True
 
-               return res
+               # If the mirror and client are in the same country, we prefer this
+               if self.country_code and self.country_code == network.country_code:
+                       return True
 
-       @property
-       def status(self):
-               if self.last_check:
-                       return self.last_check.status
+               return False
 
-       @property
-       def average_response_time(self):
-               res = self.db.get("SELECT AVG(response_time) AS response_time \
-                       FROM mirrors_checks WHERE mirror_id = %s \
-                               AND timestamp >= NOW() - '24 hours'::interval", self.id)
+       # Check
 
-               return res.response_time
+       async def check(self, force=False):
+               t = datetime.datetime.utcnow()
 
-       @property
-       def address(self):
-               return socket.gethostbyname(self.hostname)
+               # Ratelimit checks somewhat
+               if not force:
+                       # Check mirrors that are up only once an hour
+                       if self.last_check_success is True:
+                               if self.last_check_at + datetime.timedelta(hours=1) > t:
+                                       log.debug("Skipping check for %s" % self)
+                                       return
 
-       def is_preferred_for_country(self, country_code):
-               if country_code and self.country_code:
-                       return self.country_code == country_code
+                       # Check mirrors that are down once every 15 minutes
+                       elif self.last_check_success is False:
+                               if self.last_check_at + datetime.timedelta(minutes=15) > t:
+                                       log.debug("Skipping check for %s" % self)
+                                       return
 
-       @lazy_property
-       def country_code(self):
-               network = self.backend.mirrors.location.lookup(self.address)
+               log.debug("Running mirror check for %s" % self.hostname)
+
+               # Wrap this into one large transaction
+               with self.db.transaction():
+                       # Update the country code & ASN
+                       await self._update_country_code_and_asn()
+
+                       # Make URL for .timestamp
+                       url = self.make_url(".timestamp")
+
+                       response = None
+                       error = None
+
+                       # Was this check successful?
+                       success = False
+
+                       # When was the last sync?
+                       timestamp = None
+
+                       try:
+                               response = await self.backend.httpclient.fetch(
+                                       url,
 
-               if network:
-                       return network.country_code
+                                       # Allow a moment to connect and get a response
+                                       connect_timeout=10,
+                                       request_timeout=10,
+                               )
+
+                               # Try to parse the response
+                               try:
+                                       timestamp = int(response.body)
+
+                               except (TypeError, ValueError) as e:
+                                       log.error("%s responded with an invalid timestamp")
+
+                                       raise ValueError("Invalid timestamp received") from e
+
+                               # Convert into datetime
+                               timestamp = datetime.datetime.utcfromtimestamp(timestamp)
+
+                       # Catch anything that isn't 200 OK
+                       except httpclient.HTTPError as e:
+                               log.error("%s: %s" % (self, e))
+                               error = "%s" % e
+
+                       # Catch DNS Errors
+                       except socket.gaierror as e:
+                               # Name or service not known
+                               if e.code == -2:
+                                       log.error("Could not resolve %s: %s" % (self, e))
+
+                                       # Store the error
+                                       error = "%s" % e
+
+                               # Raise anything else
+                               else:
+                                       raise e
+
+                       # Success!
+                       else:
+                               # This check was successful!
+                               success = True
+
+                       # Log this check
+                       self.db.execute("""
+                               INSERT INTO
+                                       mirror_checks
+                               (
+                                       mirror_id,
+                                       success,
+                                       response_time,
+                                       http_status,
+                                       last_sync_at,
+                                       error
+                               )
+                               VALUES
+                               (
+                                       %s, %s, %s, %s, %s, %s
+                               )
+                               """,
+                               self.id,
+                               success,
+                               response.request_time if response else None,
+                               response.code if response else None,
+                               timestamp,
+                               error,
+                       )
+
+                       # Update the main table
+                       self._set_attribute_now("last_check_at")
+                       self._set_attribute("last_check_success", success)
+                       self._set_attribute("last_sync_at", timestamp)
+                       self._set_attribute("error", error)
+
+       def get_uptime_since(self, t):
+               # Convert timedeltas to absolute time
+               if isinstance(t, datetime.timedelta):
+                       t = datetime.datetime.utcnow() - t
+
+               res = self.db.get("""
+                       -- SELECT all successful checks and find out when the next one failed
+                       WITH uptimes AS (
+                               SELECT
+                                       success,
+                                       LEAST(
+                                               LEAD(checked_at, 1, CURRENT_TIMESTAMP)
+                                                       OVER (ORDER BY checked_at ASC)
+                                               - checked_at,
+                                               INTERVAL '1 hour'
+                                       ) AS uptime
+                               FROM
+                                       mirror_checks
+                               WHERE
+                                       mirror_id = %s
+                               AND
+                                       checked_at >= %s
+                       )
+                       SELECT
+                               (
+                                       EXTRACT(
+                                               epoch FROM SUM(uptime) FILTER (WHERE success IS TRUE)
+                                       )
+                                       /
+                                       EXTRACT(
+                                               epoch FROM SUM(uptime)
+                                       )
+                               ) AS uptime
+                       FROM
+                               uptimes
+                       """, self.id, t,
+               )
+
+               if res:
+                       return res.uptime or 0
 
-               return "UNKNOWN"
+               return 0
index e747b72fd4c20d4fb02b5642cf9368409fbbd69c..e08f397f739abb70de2aa7ca574530ef810d308d 100644 (file)
@@ -20,6 +20,3 @@ MAILTO=pakfire@ipfire.org
 
 # Send updates to Bugzilla
 #*/5 * * * *   _pakfire        pakfire-build-service send-bug-updates &>/dev/null
-
-# Run mirror check
-#*/30 * * * *  _pakfire        pakfire-build-service check-mirrors &>/dev/null
index b39c0d436a715902be1a99f84a130187fa45ea8d..fa6fbd2c4511527ecbba6291d4cba0b224753cfa 100644 (file)
@@ -41,6 +41,61 @@ SET default_tablespace = '';
 
 SET default_table_access_method = heap;
 
+--
+-- Name: build_comments; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.build_comments (
+    id integer NOT NULL,
+    build_id integer NOT NULL,
+    user_id integer NOT NULL,
+    text text DEFAULT ''::text NOT NULL,
+    created_at timestamp without time zone DEFAULT now() NOT NULL,
+    deleted boolean DEFAULT false NOT NULL
+);
+
+
+--
+-- Name: build_groups; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.build_groups (
+    id integer NOT NULL,
+    uuid uuid DEFAULT gen_random_uuid() NOT NULL,
+    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    created_by integer,
+    deleted_at timestamp without time zone,
+    deleted_by integer,
+    finished_at timestamp without time zone,
+    failed boolean DEFAULT false NOT NULL,
+    tested_build_id integer
+);
+
+
+--
+-- Name: build_points; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.build_points (
+    build_id integer NOT NULL,
+    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    points integer DEFAULT 0 NOT NULL,
+    user_id integer
+);
+
+
+--
+-- Name: build_watchers; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.build_watchers (
+    build_id integer NOT NULL,
+    user_id integer NOT NULL,
+    added_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    deleted_at timestamp without time zone
+);
+
+
 --
 -- Name: builds; Type: TABLE; Schema: public; Owner: -
 --
@@ -100,33 +155,17 @@ CREATE TABLE public.jobs (
 
 
 --
--- Name: build_comments; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.build_comments (
-    id integer NOT NULL,
-    build_id integer NOT NULL,
-    user_id integer NOT NULL,
-    text text DEFAULT ''::text NOT NULL,
-    created_at timestamp without time zone DEFAULT now() NOT NULL,
-    deleted boolean DEFAULT false NOT NULL
-);
-
-
---
--- Name: build_groups; Type: TABLE; Schema: public; Owner: -
+-- Name: repository_builds; Type: TABLE; Schema: public; Owner: -
 --
 
-CREATE TABLE public.build_groups (
+CREATE TABLE public.repository_builds (
     id integer NOT NULL,
-    uuid uuid DEFAULT gen_random_uuid() NOT NULL,
-    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
-    created_by integer,
-    deleted_at timestamp without time zone,
-    deleted_by integer,
-    finished_at timestamp without time zone,
-    failed boolean DEFAULT false NOT NULL,
-    tested_build_id integer
+    repo_id integer NOT NULL,
+    build_id bigint NOT NULL,
+    added_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    added_by integer,
+    removed_at timestamp without time zone,
+    removed_by integer
 );
 
 
@@ -162,18 +201,6 @@ CREATE TABLE public.build_packages (
 );
 
 
---
--- Name: build_points; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.build_points (
-    build_id integer NOT NULL,
-    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
-    points integer DEFAULT 0 NOT NULL,
-    user_id integer
-);
-
-
 --
 -- Name: build_test_builds; Type: VIEW; Schema: public; Owner: -
 --
@@ -187,18 +214,6 @@ CREATE VIEW public.build_test_builds AS
   WHERE ((builds.deleted_at IS NULL) AND (build_groups.deleted_at IS NULL));
 
 
---
--- Name: build_watchers; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.build_watchers (
-    build_id integer NOT NULL,
-    user_id integer NOT NULL,
-    added_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
-    deleted_at timestamp without time zone
-);
-
-
 --
 -- Name: builder_stats; Type: TABLE; Schema: public; Owner: -
 --
@@ -529,52 +544,42 @@ ALTER SEQUENCE public.messages_id_seq OWNED BY public.messages.id;
 
 
 --
--- Name: mirrors; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.mirrors (
-    id integer NOT NULL,
-    hostname text NOT NULL,
-    path text NOT NULL,
-    owner text,
-    contact text,
-    deleted boolean DEFAULT false NOT NULL,
-    supports_https boolean DEFAULT false NOT NULL
-);
-
-
---
--- Name: mirrors_checks; Type: TABLE; Schema: public; Owner: -
+-- Name: mirror_checks; Type: TABLE; Schema: public; Owner: -
 --
 
-CREATE TABLE public.mirrors_checks (
-    id integer NOT NULL,
+CREATE TABLE public.mirror_checks (
     mirror_id integer NOT NULL,
-    "timestamp" timestamp without time zone DEFAULT now() NOT NULL,
+    checked_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    success boolean DEFAULT false NOT NULL,
     response_time double precision,
     http_status integer,
     last_sync_at timestamp without time zone,
-    status text DEFAULT 'OK'::text NOT NULL
+    error text
 );
 
 
 --
--- Name: mirrors_checks_id_seq; Type: SEQUENCE; Schema: public; Owner: -
---
-
-CREATE SEQUENCE public.mirrors_checks_id_seq
-    START WITH 1
-    INCREMENT BY 1
-    NO MINVALUE
-    NO MAXVALUE
-    CACHE 1;
-
-
---
--- Name: mirrors_checks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+-- Name: mirrors; Type: TABLE; Schema: public; Owner: -
 --
 
-ALTER SEQUENCE public.mirrors_checks_id_seq OWNED BY public.mirrors_checks.id;
+CREATE TABLE public.mirrors (
+    id integer NOT NULL,
+    hostname text NOT NULL,
+    path text NOT NULL,
+    owner text,
+    contact text,
+    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    created_by integer NOT NULL,
+    deleted_at timestamp without time zone,
+    deleted_by integer,
+    last_check_success boolean,
+    last_check_at timestamp without time zone,
+    last_sync_at timestamp without time zone,
+    country_code text,
+    error text,
+    asn integer,
+    notes text DEFAULT ''::text NOT NULL
+);
 
 
 --
@@ -878,21 +883,6 @@ CREATE SEQUENCE public.repositories_id_seq
 ALTER SEQUENCE public.repositories_id_seq OWNED BY public.repositories.id;
 
 
---
--- Name: repository_builds; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.repository_builds (
-    id integer NOT NULL,
-    repo_id integer NOT NULL,
-    build_id bigint NOT NULL,
-    added_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
-    added_by integer,
-    removed_at timestamp without time zone,
-    removed_by integer
-);
-
-
 --
 -- Name: repository_builds_id_seq; Type: SEQUENCE; Schema: public; Owner: -
 --
@@ -1211,13 +1201,6 @@ ALTER TABLE ONLY public.messages ALTER COLUMN id SET DEFAULT nextval('public.mes
 ALTER TABLE ONLY public.mirrors ALTER COLUMN id SET DEFAULT nextval('public.mirrors_id_seq'::regclass);
 
 
---
--- Name: mirrors_checks id; Type: DEFAULT; Schema: public; Owner: -
---
-
-ALTER TABLE ONLY public.mirrors_checks ALTER COLUMN id SET DEFAULT nextval('public.mirrors_checks_id_seq'::regclass);
-
-
 --
 -- Name: packages id; Type: DEFAULT; Schema: public; Owner: -
 --
@@ -1344,14 +1327,6 @@ ALTER TABLE ONLY public.images_types
     ADD CONSTRAINT idx_2198057_primary PRIMARY KEY (id);
 
 
---
--- Name: mirrors idx_2198115_primary; Type: CONSTRAINT; Schema: public; Owner: -
---
-
-ALTER TABLE ONLY public.mirrors
-    ADD CONSTRAINT idx_2198115_primary PRIMARY KEY (id);
-
-
 --
 -- Name: users idx_2198244_primary; Type: CONSTRAINT; Schema: public; Owner: -
 --
@@ -1385,11 +1360,11 @@ ALTER TABLE ONLY public.messages
 
 
 --
--- Name: mirrors_checks mirrors_checks_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+-- Name: mirrors mirrors_pkey; Type: CONSTRAINT; Schema: public; Owner: -
 --
 
-ALTER TABLE ONLY public.mirrors_checks
-    ADD CONSTRAINT mirrors_checks_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.mirrors
+    ADD CONSTRAINT mirrors_pkey PRIMARY KEY (id);
 
 
 --
@@ -1670,12 +1645,17 @@ CREATE INDEX messages_queued ON public.messages USING btree (priority DESC, queu
 
 
 --
--- Name: mirrors_checks_sort; Type: INDEX; Schema: public; Owner: -
+-- Name: mirror_checks_search; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX mirrors_checks_sort ON public.mirrors_checks USING btree (mirror_id, "timestamp");
+CREATE INDEX mirror_checks_search ON public.mirror_checks USING btree (mirror_id, checked_at);
+
+
+--
+-- Name: mirrors_hostname; Type: INDEX; Schema: public; Owner: -
+--
 
-ALTER TABLE public.mirrors_checks CLUSTER ON mirrors_checks_sort;
+CREATE UNIQUE INDEX mirrors_hostname ON public.mirrors USING btree (hostname) WHERE (deleted_at IS NULL);
 
 
 --
@@ -2044,11 +2024,27 @@ ALTER TABLE ONLY public.keys
 
 
 --
--- Name: mirrors_checks mirrors_checks_mirror_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+-- Name: mirror_checks mirror_checks_mirror_id; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
 
-ALTER TABLE ONLY public.mirrors_checks
-    ADD CONSTRAINT mirrors_checks_mirror_id FOREIGN KEY (mirror_id) REFERENCES public.mirrors(id);
+ALTER TABLE ONLY public.mirror_checks
+    ADD CONSTRAINT mirror_checks_mirror_id FOREIGN KEY (mirror_id) REFERENCES public.mirrors(id);
+
+
+--
+-- Name: mirrors mirrors_created_by; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.mirrors
+    ADD CONSTRAINT mirrors_created_by FOREIGN KEY (created_by) REFERENCES public.users(id);
+
+
+--
+-- Name: mirrors mirrors_deleted_by; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.mirrors
+    ADD CONSTRAINT mirrors_deleted_by FOREIGN KEY (deleted_by) REFERENCES public.users(id);
 
 
 --
index ac5256e2291030305522b8f3ba97a34373407b0f..8622a4266a63845e2ab998bb94c89cabd850ff92 100644 (file)
@@ -37,6 +37,9 @@ class Cli(object):
                        # Messages
                        "messages:queue:send" : self.backend.messages.queue.send,
 
+                       # Mirrors
+                       "mirrors:check"       : self._mirrors_check,
+
                        # Release Monitoring
                        "releasemonitoring:update"  : self._release_monitoring_update,
 
@@ -49,9 +52,6 @@ class Cli(object):
                        # Sync
                        "sync"                : self.backend.sync,
 
-                       # Run mirror check
-                       #"check-mirrors" : self.backend.mirrors.check,
-
                        # Dist
                        #"dist" : self.backend.sources.dist,
 
@@ -191,6 +191,12 @@ class Cli(object):
                                if release:
                                        print("  Found new release: %s" % release)
 
+       async def _mirrors_check(self):
+               """
+                       Forces a check on all mirrors
+               """
+               return await self.backend.mirrors.check(force=True)
+
 
 async def main():
        cli = Cli()
index 53b6fcac2970edd4989d428347ab9ef0d5088939..8961e1b19a188673fb27147e13f3475d67cf9d85 100644 (file)
@@ -1,51 +1,43 @@
-{% extends "../base.html" %}
+{% extends "../modal.html" %}
 
-{% block title %}{{ _("Delete mirror %s") % mirror.hostname }}{% end block %}
+{% block title %}{{ _("Delete Mirror") }} - {{ mirror }}{% end block %}
 
-{% block body %}
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <nav aria-label="breadcrumb" role="navigation">
-                               <ol class="breadcrumb">
-                                       <li class="breadcrumb-item"><a href="/">{{ _("Home") }}</a></li>
-                                       <li class="breadcrumb-item"><a href="/mirrors">{{ _("Mirrors") }}</a></li>
-                                       <li class="breadcrumb-item">
-                                               <a href="/mirror/{{ mirror.hostname }}">{{ mirror.hostname }}</a>
-                                       </li>
-                                       <li class="breadcrumb-item active">
-                                               <a href="/mirror/{{ mirror.hostname }}/delete">{{ _("Delete") }}</a>
-                                       </li>
-                               </ol>
-                       </nav>
-               </div>
-       </div>
+{% block breadcrumbs %}
+       <nav class="breadcrumb" aria-label="breadcrumbs">
+               <ul>
+                       <li>
+                               <a href="/mirrors">{{ _("Mirrors") }}</a>
+                       </li>
+                       <li>
+                               <a href="/mirrors/{{ mirror.hostname }}">{{ mirror }}</a>
+                       </li>
+                       <li class="is-active">
+                               <a href="#" aria-current="page">{{ _("Delete") }}</a>
+                       </li>
+               </ul>
+       </nav>
+{% end block %}
 
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <div class="alert alert-danger" role="alert">
-                               <h2 class="alert-heading" style="word-wrap: break-word;">
-                                       {{ _("Delete mirror: %s") % mirror.hostname }}
-                               </h2>
-                               <p>
-                                       {{ _("You are going to delete the mirror %s.") % mirror.hostname }}
-                               </p>
-                       </div>
-               </div>
-       </div>
+{% block modal_title %}
+       <h4 class="title is-4">{{ _("Delete Mirror") }}</h4>
+       <h6 class="subtitle is-6">{{ mirror }}</h6>
+{% end block %}
+
+{% block modal %}
+       <form method="POST" action="">
+               {% raw xsrf_form_html() %}
 
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <div class="btn-toolbar" role="toolbar">
-                               <div class="btn-group mr-2" role="group">
-                                       <a class="btn btn-danger" href="/mirror/{{ mirror.hostname }}/delete?confirmed=1">
-                                               {{ _("Delete %s") % mirror.hostname }}
-                                       </a>
-                               </div>
-                               <div class="btn-group" role="group">
-                                       <a class="btn btn-primary" href="/mirror/{{ mirror.hostname }}">{{ _("Cancel") }}</a>
-                               </div>
-                       </div>
+               <div class="content">
+                       <p>
+                               {{ _("Are you sure you want to delete %s?") % mirror }}
+                       </p>
                </div>
-       </div>
 
+               {# Submit! #}
+               <div class="field">
+                       <button type="submit" class="button is-danger is-fullwidth">
+                               {{ _("Delete Mirror") }}
+                       </button>
+               </div>
+       </form>
 {% end block %}
diff --git a/src/templates/mirrors/detail.html b/src/templates/mirrors/detail.html
deleted file mode 100644 (file)
index 40c632c..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Mirror: %s") % mirror.hostname }}{% end block %}
-
-{% block body %}
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <nav aria-label="breadcrumb" role="navigation">
-                               <ol class="breadcrumb">
-                                       <li class="breadcrumb-item"><a href="/">{{ _("Home") }}</a></li>
-                                       <li class="breadcrumb-item"><a href="/mirrors">{{ _("Mirrors") }}</a></li>
-                                       <li class="breadcrumb-item active">
-                                               <a href="/mirror/{{ mirror.hostname }}">{{ mirror.hostname }}</a>
-                                       </li>
-                               </ol>
-                       </nav>
-               </div>
-       </div>
-
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-9 col-lg-10 col-xl-10">
-                       <h2 style="word-wrap: break-word;">
-                               {{ _("Mirror: %s") % mirror.hostname }} <br>
-                               <small class="text-muted">{{ mirror.owner }}</small>
-                       </h2>
-               </div>
-               {% if mirror.has_perm(current_user) %}
-                       <div class="col-12 col-sm-12 col-md-3 col-lg-2 col-xl-2">
-                               <button class="btn dropdown-toggle btn-block " type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                                       {{ _("Actions") }}
-                               </button>
-                               <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                                       <a class="dropdown-item" href="/mirror/new">
-                                               <i class="icon-asterisk"></i> {{ _("Add new mirror") }}
-                                       </a>
-                                       <a class="dropdown-item" href="/mirror/{{ mirror.hostname }}/edit">
-                                               <i class="icon-edit"></i>
-                                               {{ _("Edit settings") }}
-                                       </a>
-                                       <a class="dropdown-item" href="/mirror/{{ mirror.hostname }}/delete">
-                                               <i class="icon-trash"></i>
-                                               {{ _("Delete mirror") }}
-                                       </a>
-                               </div>
-                       </div>
-               {% end %}
-       </div>
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <h3>{{ _("General") }}</h3>
-                       <div class="table-responsive">
-                               <table class="table table-striped table-hover">
-                                       <tbody>
-                                               <tr>
-                                                       <td>{{ _("Hostname") }}</td>
-                                                       <td>{{ mirror.hostname }}</td>
-                                               </tr>
-                                               <tr>
-                                                       <td>{{ _("Location") }}</td>
-                                                       <td class="text-muted">{{ _("The location of the mirror server could not be estimated.") }}</td>
-                                               </tr>
-
-                                               {% if mirror.has_perm(current_user) %}
-                                                       <tr>
-                                                               <td>{{ _("Contact") }}</td>
-                                                               <td>
-                                                                       {% if mirror.contact %}
-                                                                               <a href="mailto:{{ mirror.contact }}">{{ mirror.contact }}</a>
-                                                                       {% else %}
-                                                                               {{ _("N/A") }}
-                                                                       {% end %}
-                                                               </td>
-                                                       </tr>
-                                               {% end %}
-                                       </tbody>
-                               </table>
-                       </div>
-               </div>
-       </div>
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <h3>{{ _("Status information") }}</h3>
-                       <div class="table-responsive">
-                               <table class="table table-striped table-hover">
-                                       <tbody>
-                                               {% if not mirror.status == "OK" %}
-                                                       <tr>
-                                                               <td>{{ _("Status") }}</td>
-                                                               <td>{{ mirror.status }}</td>
-                                                       </tr>
-
-                                                       {% if mirror.status == "ERROR" %}
-                                                               <tr>
-                                                                       <td>{{ _("HTTP Response Code") }}</td>
-                                                                       <td>{{ mirror.last_check.http_status }}</td>
-                                                               </tr>
-                                                       {% end %}
-
-                                                       {% if mirror.last_check and mirror.last_check.last_sync_at %}
-                                                               <tr>
-                                                                       <td>{{ _("Last sync") }}</td>
-                                                                       <td>
-                                                                               {{ locale.format_date(mirror.last_check.last_sync_at) }}
-                                                                       </td>
-                                                               </tr>
-                                                       {% end %}
-
-                                                       <tr>
-                                                               <td>{{ _("Last check") }}</td>
-                                                               <td>
-                                                                       {% if mirror.last_check %}
-                                                                               {{ format_date(mirror.last_check.timestamp) }}
-                                                                       {% else %}
-                                                                               {{ _("Never") }}
-                                                                       {% end %}
-                                                               </td>
-                                                       </tr>
-                                               {% end %}
-
-                                               {% if mirror.average_response_time %}
-                                                       <tr>
-                                                               <td>{{ _("Average Response Time") }}</td>
-                                                               <td>
-                                                                       {{ "%.2fms" % (mirror.average_response_time * 1000) }}
-                                                               </td>
-                                                       </tr>
-                                               {% end %}
-                                       </tbody>
-                               </table>
-                       </div>
-               </div>
-       </div>
-{% end block %}
index a17aa03d15a94e70f847362de310ee6f57d01c6a..368e9512464b6909448902295e087cfd45bd6db7 100644 (file)
-{% extends "../base.html" %}
+{% extends "../modal.html" %}
 
-{% block title %}{{ _("Manage mirror %s") % mirror.hostname }}{% end block %}
+{% block title %}
+       {% if mirror %}
+               {{ _("Edit Mirror") }} - {{ mirror }}
+       {% else %}
+               {{ _("Create Mirror") }}
+       {% end %}
+{% end block %}
+
+{% block breadcrumbs %}
+       <nav class="breadcrumb" aria-label="breadcrumbs">
+               <ul>
+                       <li>
+                               <a href="/mirrors">{{ _("Mirrors") }}</a>
+                       </li>
+
+                       {% if mirror %}
+                               <li>
+                                       <a href="/mirrors/{{ mirror.hostname }}">{{ mirror }}</a>
+                               </li>
+                               <li class="is-active">
+                                       <a href="#" aria-current="page">{{ _("Edit") }}</a>
+                               </li>
+                       {% else %}
+                               <li class="is-active">
+                                       <a href="#" aria-current="page">{{ _("Create") }}</a>
+                               </li>
+                       {% end %}
+               </ul>
+       </nav>
+{% end block %}
+
+{% block modal_title %}
+       {% if mirror %}
+               <h4 class="title is-4">{{ _("Edit Mirror") }}</h4>
+               <h6 class="subtitle is-6">{{ mirror }}</h6>
+       {% else %}
+               <h4 class="title is-4">{{ _("Create A New Mirror") }}</h4>
+       {% end %}
+{% end block %}
+
+{% block modal %}
+       <form method="POST" action="">
+               {% raw xsrf_form_html() %}
+
+               {# Hostname & Path can only be set once #}
+               {% if not mirror %}
+                       {# Hostname #}
+                       <div class="field">
+                               <label class="label">{{ _("Hostname") }}</label>
+                               <div class="control">
+                                       <input class="input" type="text" name="hostname"
+                                               placeholder="{{ _("Hostname") }}" required>
+                               </div>
+                       </div>
 
-{% block body %}
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <nav aria-label="breadcrumb" role="navigation">
-                               <ol class="breadcrumb">
-                                       <li class="breadcrumb-item"><a href="/">{{ _("Home") }}</a></li>
-                                       <li class="breadcrumb-item"><a href="/mirrors">{{ _("Mirrors") }}</a></li>
-                                       <li class="breadcrumb-item">
-                                               <a href="/mirror/{{ mirror.hostname }}">{{ mirror.hostname }}</a>
-                                       </li>
-                                       <li class="breadcrumb-item active">
-                                                       <a href="/mirror/{{ mirror.hostname }}/edit">{{ _("Manage") }}</a>
-                                       </li>
-                               </ol>
-                       </nav>
+                       {# Path #}
+                       <div class="field">
+                               <label class="label">{{ _("Path") }}</label>
+                               <div class="control">
+                                       <input class="input" type="text" name="path"
+                                               placeholder="{{ _("Path") }}" required>
+                               </div>
+                       </div>
+               {% end %}
+
+               {# Owner #}
+               <div class="field">
+                       <label class="label">{{ _("Owner") }}</label>
+                       <div class="control">
+                               <input class="input" type="text" name="owner"
+                                       {% if mirror %}value="{{ mirror.owner }}"{% end %}
+                                       placeholder="{{ _("Owner") }}" required>
+                       </div>
                </div>
-       </div>
 
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <h2 style="word-wrap: break-word;">
-                               {{ _("Manage mirror: %s") % mirror.hostname }}
-                       </h2>
+               {# Contact #}
+               <div class="field">
+                       <label class="label">{{ _("Contact Email Address") }}</label>
+                       <div class="control">
+                               <input class="input" type="email" name="contact"
+                                       {% if mirror %}value="{{ mirror.contact }}"{% end %}
+                                       placeholder="{{ _("Contact Email Address") }}" required>
+                       </div>
                </div>
-       </div>
 
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <form method="POST" action="">
-                               {% raw xsrf_form_html() %}
-                               <fieldset>
-                                       <div class="form-group">
-                                               <label for="name">{{ _("Hostname") }}</label>
-                                               <input type="text" class="form-control" id="name" name="name"
-                                                       aria-describedby="nameHelp" value="{{ mirror.hostname }}">
-                                               <small id="nameHelp" class="form-text text-muted">
-                                                       {{ _("The canonical hostname.") }}
-                                               </small>
-                                       </div>
-                                       <div class="form-check">
-                                               <label class="form-check-label">
-                                                       <input class="form-check-input" type="checkbox" name="supports_https" {% if mirror.supports_https %}checked{% end %}>
-                                                       {{ _("Check if this mirror server supports HTTPS.") }}
-                                               </label>
-                                       </div>
-                               </fieldset>
-                               <fieldset>
-                                       <legend>{{ _("Contact information") }}</legend>
-                                       <div class="form-group">
-                                               <label for="owner">{{ _("Owner") }}</label>
-                                               <input type="text" class="form-control" id="owner" name="owner"
-                                                       aria-describedby="ownerHelp" placeholder="{{ _("Owner") }}" value="{{ mirror.owner }}">
-                                               <small id="ownerHelp" class="form-text text-muted">
-                                                       {{ _("The owner of the mirror server.") }}
-                                               </small>
-                                       </div>
-                                       <div class="form-group">
-                                               <label for="contact">{{ _("Contact address") }}</label>
-                                               <input type="text" class="form-control" id="contact" name="contact"
-                                                       aria-describedby="contactHelp" placeholder="{{ _("Contact address") }}" value="{{ mirror.contact }}">
-                                               <small id="contactHelp" class="form-text text-muted">
-                                                               {{ _("An email address to contact an administrator of the mirror.") }}
-                                                               <br>
-                                                               <em>{{ _("This won't be made public.") }}</em>
-                                               </small>
-                                       </div>
+               {# Notes #}
+               <div class="field">
+                       <label class="label">{{ _("Notes") }}</label>
+                       <div class="control">
+                               <textarea class="textarea" name="notes" rows="8"
+                                       >{% if mirror %}{{ mirror.notes }}{% end %}</textarea>
+                       </div>
+               </div>
 
-                               </fieldset>
-                               <button type="submit" class="btn btn-primary">{{ _("Save changes") }}</button>
-                               <a class="btn" href="/mirror/{{ mirror.hostname }}">{{ _("Cancel") }}</a>
-                       </form>
+               {# Submit! #}
+               <div class="field">
+                       {% if mirror %}
+                               <button type="submit" class="button is-warning is-fullwidth">
+                                       {{ _("Save") }}
+                               </button>
+                       {% else %}
+                               <button type="submit" class="button is-success is-fullwidth">
+                                       {{ _("Create Mirror") }}
+                               </button>
+                       {% end %}
                </div>
-       </div>
+       </form>
 {% end block %}
diff --git a/src/templates/mirrors/index.html b/src/templates/mirrors/index.html
new file mode 100644 (file)
index 0000000..96e8221
--- /dev/null
@@ -0,0 +1,37 @@
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Mirrors") }}{% end block %}
+
+{% block body %}
+       <section class="hero is-light">
+               <div class="hero-body">
+                       <div class="container">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
+                                       <ul>
+                                               <li class="is-active">
+                                                       <a href="#" aria-current="page">
+                                                               {{ _("Mirrors") }}
+                                                       </a>
+                                               </li>
+                                       </ul>
+                               </nav>
+
+                               <h1 class="title">{{ _("Mirrors") }}</h1>
+                       </div>
+               </div>
+       </section>
+
+       <section class="section">
+               <div class="container">
+                       {% module MirrorsList(mirrors) %}
+
+                       {% if current_user and current_user.is_admin() %}
+                               <div class="block">
+                                       <a class="button is-success" href="/mirrors/create">
+                                               {{ _("Create Mirror") }}
+                                       </a>
+                               </div>
+                       {% end %}
+               </div>
+       </section>
+{% end block %}
diff --git a/src/templates/mirrors/list.html b/src/templates/mirrors/list.html
deleted file mode 100644 (file)
index 27e4161..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Mirrors") }}{% end block %}
-
-{% block body %}
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <nav aria-label="breadcrumb" role="navigation">
-                               <ol class="breadcrumb">
-                                       <li class="breadcrumb-item"><a href="/">{{ _("Home") }}</a></li>
-                                       <li class="breadcrumb-item active"><a href="/mirrors">{{ _("Mirrors") }}</a></li>
-                               </ol>
-                       </nav>
-               </div>
-       </div>
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-3 col-lg-10 col-xl-10">
-                       <h2>{{ _("Mirrors") }}</h2>
-               </div>
-               {% if current_user and current_user.is_admin() %}
-                       <div class="col-12 col-sm-12 col-md-3 col-lg-2 col-xl-2">
-                               <button class="btn dropdown-toggle btn-block" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                                       {{ _("Actions") }}
-                               </button>
-                               <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                                               <a class="dropdown-item" href="/mirror/new">
-                                                       <i class="icon-asterisk"></i> {{ _("Add new mirror") }}
-                                               </a>
-                               </div>
-                       </div>
-               {% end %}
-       </div>
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <p>
-                               {{ _("On this page, you will see a list of all mirror servers.") }}
-                       </p>
-               </div>
-       </div>
-
-       {% if mirrors %}
-               <div class="row">
-                       <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                               <div class="table-responsive">
-                                       <table class="table table-striped table-hover">
-                                               <thead>
-                                                       <tr>
-                                                               <th>{{ _("Hostname") }} / {{ _("Owner") }}</th>
-                                                               <th></th>
-                                                               <th>{{ _("Last check") }}</th>
-                                                       </tr>
-                                               </thead>
-                                               <tbody>
-                                                       {% for mirror in mirrors %}
-                                                               <tr>
-                                                                       <td>
-                                                                               <a href="/mirror/{{ mirror.hostname }}">
-                                                                                       {{ mirror.hostname }}
-                                                                               </a>
-                                                                               <p class="text-muted">
-                                                                                       {{ mirror.owner or _("N/A") }}
-                                                                               </p>
-                                                                       </td>
-                                                                       <td>
-                                                                               [{{ mirror.country_code }}] -
-
-                                                                               {% if mirror.status == "OK" %}
-                                                                                       <span class="text-success">
-                                                                                               {{ _("Up") }}
-                                                                                       </span>
-                                                                               {% elif mirror.status == "OUTOFSYNC" %}
-                                                                                       <span class="text-warning">
-                                                                                               {{ _("Out Of Sync") }}
-                                                                                       </span>
-                                                                               {% elif mirror.status == "ERROR" %}
-                                                                                       <span class="text-danger">
-                                                                                               {{ _("Down") }}
-                                                                                       </span>
-                                                                               {% else %}
-                                                                                       <span class="text-muted">
-                                                                                               {{ _("Unknown") }}
-                                                                                       </span>
-                                                                               {% end %}
-                                                                       </td>
-
-                                                                       <td>
-                                                                               {% if mirror.last_check %}
-                                                                                       {{ format_date(mirror.last_check.timestamp, relative=True) }}
-                                                                               {% else %}
-                                                                                       {{ _("N/A") }}
-                                                                               {% end %}
-                                                                       </td>
-                                                               </tr>
-                                                       {% end %}
-                                               </tbody>
-                                       </table>
-                               </div>
-       {% else %}
-                               <p class="muted">
-                                       {{ _("There are no mirrors configured, yet.") }}
-                               </p>
-                       </div>
-               </div>
-       {% end %}
-{% end block %}
diff --git a/src/templates/mirrors/modules/list.html b/src/templates/mirrors/modules/list.html
new file mode 100644 (file)
index 0000000..edccfd4
--- /dev/null
@@ -0,0 +1,15 @@
+<div class="block">
+       {% for mirror in mirrors %}
+               <div class="box">
+                       <h5 class="title is-5">
+                               <a href="/mirrors/{{ mirror.hostname }}">
+                                       {{ mirror }}
+                               </a>
+                       </h5>
+
+                       {% if mirror.owner %}
+                               <h6 class="subtitle is-6">{{ mirror.owner }}</h6>
+                       {% end %}
+               </div>
+       {% end %}
+</div>
diff --git a/src/templates/mirrors/new.html b/src/templates/mirrors/new.html
deleted file mode 100644 (file)
index c944696..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-{% extends "../base.html" %}
-
-{% block title %}{{ _("Add new mirror") }}{% end block %}
-
-{% block body %}
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <nav aria-label="breadcrumb" role="navigation">
-                               <ol class="breadcrumb">
-                                       <li class="breadcrumb-item"><a href="/">{{ _("Home") }}</a></li>
-                                       <li class="breadcrumb-item"><a href="/mirrors">{{ _("Mirrors") }}</a></li>
-                                       <li class="breadcrumb-item active">
-                                               <a href="/mirror/new">{{ _("New mirror") }}</a>
-                                       </li>
-                               </ol>
-                       </nav>
-               </div>
-       </div>
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <h2 style="word-wrap: break-word;">
-                               {{ _("Add a new mirror") }}
-                       </h2>
-               </div>
-       </div>
-
-       <div class="row">
-               <div class="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
-                       <form method="POST" action="">
-                               {% raw xsrf_form_html() %}
-                               <fieldset>
-                                       <div class="form-group {% if hostname_missing %}is-invalid{% end %}">
-                                               <label for="name">{{ _("Hostname") }}</label>
-                                               <input type="text" class="form-control" id="name" name="name"
-                                                       aria-describedby="nameHelp" placeholder="{{ _("Hostname") }}" {% if _hostname %}value="{{ _hostname }}"{% end %}>
-                                               <small id="nameHelp" class="form-text text-muted">
-                                                       {{ _("Enter the canonical hostname of the mirror.") }}
-                                               </small>
-                                       </div>
-
-                                       <div class="form-group {% if path_invalid  %}is-invalid{% end %}">
-                                               <label for="path">{{ _("Path") }}</label>
-                                               <input type="text" class="form-control"  id="path" name="path"
-                                                       aria-describedby="pathHelp" placeholder="{{ _("Path") }}" {% if path %}value="{{ path }}"{% end %}>
-                                               <small id="pathHelp" class="form-text text-muted">
-                                                       {{ _("The path to the files on the server.") }}
-                                               </small>
-                                       </div>
-                                       <button type="submit" class="btn btn-primary">{{ _("Create new mirror") }}</button>
-                               </fieldset>
-                       </form>
-               </div>
-       </div>
-{% end block %}
diff --git a/src/templates/mirrors/show.html b/src/templates/mirrors/show.html
new file mode 100644 (file)
index 0000000..2ca2106
--- /dev/null
@@ -0,0 +1,153 @@
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Mirrors") }} - {{ mirror }}{% end block %}
+
+{% block body %}
+       <section class="hero is-light">
+               <div class="hero-body">
+                       <div class="container">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
+                                       <ul>
+                                               <li>
+                                                       <a href="/mirrors">{{ _("Mirrors") }}</a>
+                                               </li>
+                                               <li class="is-active">
+                                                       <a href="#" aria-current="page">{{ mirror }}</a>
+                                               </li>
+                                       </ul>
+                               </nav>
+
+                               <h1 class="title is-1">{{ mirror }}</h1>
+
+                               {% if mirror.owner %}
+                                       <h4 class="subtitle is-4">{{ mirror.owner }}</h4>
+                               {% end %}
+
+                               <div class="level">
+                                       <div class="level-item has-text-centered">
+                                               <div>
+                                                       <p class="heading">{{ _("Status") }}</p>
+                                                       <p>
+                                                               {% if mirror.last_check_success is True %}
+                                                                       <span class="tag is-success">{{ _("Online") }}</span>
+                                                               {% elif mirror.last_check_success is False %}
+                                                                       <span class="tag is-danger">{{ _("Offline") }}</span>
+                                                               {% else %}
+                                                                       <span class="tag">{{ _("Pending") }}<span>
+                                                               {% end %}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       {# ASN #}
+                                       {% if mirror.asn %}
+                                               <div class="level-item has-text-centered">
+                                                       <div>
+                                                               <p class="heading">{{ _("Autonomous System") }}</p>
+                                                               <p>
+                                                                       {{ mirror.asn }}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       {% end %}
+
+                                       {# Country Code #}
+                                       {% if mirror.country_code %}
+                                               <div class="level-item has-text-centered">
+                                                       <div>
+                                                               <p class="heading">{{ _("Location") }}</p>
+                                                               <p>
+                                                                       {{ mirror.country_code }}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       {% end %}
+
+                                       {# Last Check #}
+                                       {% if mirror.last_check_at %}
+                                               <div class="level-item has-text-centered">
+                                                       <div>
+                                                               <p class="heading">{{ _("Last Check") }}</p>
+                                                               <p>
+                                                                       {{ locale.format_date(mirror.last_check_at, shorter=True) }}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       {% end %}
+
+                                       {# Uptime #}
+                                       {% set uptime = mirror.get_uptime_since(datetime.timedelta(days=30)) %}
+                                       {% if uptime is not None %}
+                                               <div class="level-item has-text-centered">
+                                                       <div>
+                                                               <p class="heading">{{ _("Uptime In The Last 30 Days") }}</p>
+                                                               <p>
+                                                                       {% if uptime >= 0.99 %}
+                                                                               <span class="has-text-success">{{ "%.4f%%" % (uptime * 100) }}</span>
+                                                                       {% elif uptime >= 0.90 %}
+                                                                               <span class="has-text-warning">{{ "%.4f%%" % (uptime * 100) }}</span>
+                                                                       {% else %}
+                                                                               <span class="has-text-danger">{{ "%.4f%%" % (uptime * 100) }}</span>
+                                                                       {% end %}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       {% end %}
+                               </div>
+                       </div>
+               </div>
+       </section>
+
+       {% if mirror.has_perm(current_user) %}
+               <section class="section">
+                       <div class="container">
+                               {# Errors #}
+                               {% if mirror.last_check_success is False %}
+                                       <div class="block">
+                                               <article class="message is-danger">
+                                                       <div class="message-header">
+                                                               <p>{{ _("Mirror Check Failed") }}</p>
+                                                       </div>
+                                                       <div class="message-body">
+                                                               {{ mirror.error }}
+                                                       </div>
+                                               </article>
+                                       </div>
+                               {% end %}
+
+                               {# Notes #}
+                               {% if mirror.notes %}
+                                       <div class="block">
+                                               <div class="notification">
+                                                       {% module Text(mirror.notes) %}
+                                               </div>
+                                       </div>
+                               {% end %}
+
+                               <div class="buttons">
+                                       <form id="check" method="POST" action="/mirrors/{{ mirror.hostname }}/check">
+                                               {% raw xsrf_form_html() %}
+                                       </form>
+
+                                       <button class="button is-light" type="submit" form="check">
+                                               {{ _("Check Now") }}
+                                       </button>
+
+                                       {% if mirror.contact %}
+                                               <a class="button is-info" href="mailto:{{ mirror.contact }}">
+                                                       {{ _("Contact Owner") }}
+                                               </a>
+                                       {% end %}
+
+                                       <a class="button is-warning" href="/mirrors/{{ mirror.hostname }}/edit">
+                                               {{ _("Edit") }}
+                                       </a>
+
+                                       <a class="button is-danger" href="/mirrors/{{ mirror.hostname }}/delete">
+                                               {{ _("Delete") }}
+                                       </a>
+                               </div>
+                       </div>
+               </section>
+       {% end %}
+{% end block %}
index aef1d43520171d4dffa9142dc6aa80d47b8195f3..5d244269cf0ec58b7b7e559f84e691c75d43fdb1 100644 (file)
@@ -67,6 +67,9 @@ class Application(tornado.web.Application):
                                "JobsList"           : jobs.ListModule,
                                "JobsLogStream"      : jobs.LogStreamModule,
 
+                               # Mirrors
+                               "MirrorsList"        : mirrors.ListModule,
+
                                # Packages
                                "PackageInfo"        : packages.InfoModule,
                                "PackageDependencies": packages.DependenciesModule,
@@ -187,11 +190,12 @@ class Application(tornado.web.Application):
                                repos.MirrorlistHandler),
 
                        # Mirrors
-                       (r"/mirrors",                                   mirrors.MirrorListHandler),
-                       (r"/mirror/new",                                mirrors.MirrorNewHandler),
-                       (r"/mirror/([\w\-\.]+)/delete", mirrors.MirrorDeleteHandler),
-                       (r"/mirror/([\w\-\.]+)/edit",   mirrors.MirrorEditHandler),
-                       (r"/mirror/([\w\-\.]+)",                mirrors.MirrorDetailHandler),
+                       (r"/mirrors",                                    mirrors.IndexHandler),
+                       (r"/mirrors/create",             mirrors.CreateHandler),
+                       (r"/mirrors/([\w\-\.]+)",                mirrors.ShowHandler),
+                       (r"/mirrors/([\w\-\.]+)/check",  mirrors.CheckHandler),
+                       (r"/mirrors/([\w\-\.]+)/delete", mirrors.DeleteHandler),
+                       (r"/mirrors/([\w\-\.]+)/edit",   mirrors.EditHandler),
 
                        # Keys
                        (r"/keys/([A-Z0-9]+)", keys.DownloadHandler),
@@ -217,6 +221,9 @@ class Application(tornado.web.Application):
                self.backend.run_task(self.backend.builders.sync)
                self.backend.run_task(self.backend.builders.autoscale)
 
+               # Regularly check the mirrors
+               self.backend.run_periodic_task(300, self.backend.mirrors.check)
+
        ## UI methods
 
        def extract_hostname(self, handler, url):
index 602d1b498865a99761478391b45bd0d4a213791f..7419bbdaf69ef7df975008686fb59471ee45ddb2 100644 (file)
@@ -3,61 +3,80 @@
 import tornado.web
 
 from . import base
+from . import ui_modules
 
-class MirrorListHandler(base.BaseHandler):
+class IndexHandler(base.BaseHandler):
        def get(self):
-               self.render("mirrors/list.html", mirrors=self.backend.mirrors)
+               self.render("mirrors/index.html", mirrors=self.backend.mirrors)
 
 
-class MirrorDetailHandler(base.BaseHandler):
+class ShowHandler(base.BaseHandler):
        def get(self, hostname):
                mirror = self.backend.mirrors.get_by_hostname(hostname)
                if not mirror:
-                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+                       raise tornado.web.HTTPError(404, "Could not find mirror %s" % hostname)
 
-               self.render("mirrors/detail.html", mirror=mirror)
+               self.render("mirrors/show.html", mirror=mirror)
 
 
-class MirrorNewHandler(base.BaseHandler):
-       # XXX everyone can perform this task because of a lacking permission check
-
+class CheckHandler(base.BaseHandler):
        @tornado.web.authenticated
-       def get(self, hostname="", path="", hostname_missing=False, path_invalid=False):
-               self.render("mirrors/new.html", _hostname=hostname, path=path,
-                       hostname_missing=hostname_missing, path_invalid=path_invalid)
+       async def post(self, hostname):
+               mirror = self.backend.mirrors.get_by_hostname(hostname)
+               if not mirror:
+                       raise tornado.web.HTTPError(404, "Could not find mirror %s" % hostname)
 
-       @tornado.web.authenticated
-       def post(self):
-               errors = {}
+               # Check permissions
+               if not mirror.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403, "%s has no permission for %s" \
+                               % (self.current_user, mirror))
 
-               hostname = self.get_argument("name", None)
-               if not hostname:
-                       errors["hostname_missing"] = True
+               # check() creates its own transaction
+               await mirror.check(force=True)
 
-               path = self.get_argument("path", "")
-               if path is None:
-                       errors["path_invalid"] = True
+               # Redirect back to the mirror
+               self.redirect("/mirrors/%s" % mirror.hostname)
 
-               if errors:
-                       errors.update({
-                               "hostname" : hostname,
-                               "path" : path,
-                       })
-                       return self.get(**errors)
 
-               # Create mirror
+class CreateHandler(base.BaseHandler):
+       def prepare(self):
+               # Admin permissions are required
+               if not self.current_user.is_admin():
+                       raise tornado.web.HTTPError(403)
+
+       @tornado.web.authenticated
+       def get(self):
+               self.render("mirrors/edit.html", mirror=None)
+
+       @tornado.web.authenticated
+       async def post(self):
+               # Fetch all values
+               hostname = self.get_argument("hostname")
+               path     = self.get_argument("path")
+               owner    = self.get_argument("owner")
+               contact  = self.get_argument("contact")
+               notes    = self.get_argument("notes", None)
+
+               # Create the mirror
                with self.db.transaction():
-                       mirror = self.backend.mirrors.create(hostname, path, user=self.current_user)
+                       mirror = await self.backend.mirrors.create(
+                               hostname,
+                               path,
+                               owner,
+                               contact,
+                               user=self.current_user,
+                       )
 
-               self.redirect("/mirror/%s" % mirror.hostname)
+               # Redirect the user back
+               self.redirect("/mirrors/%s" % mirror.hostname)
 
 
-class MirrorEditHandler(base.BaseHandler):
+class EditHandler(base.BaseHandler):
        @tornado.web.authenticated
        def get(self, hostname):
                mirror = self.backend.mirrors.get_by_hostname(hostname)
                if not mirror:
-                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+                       raise tornado.web.HTTPError(404, "Could not find mirror %s" % hostname)
 
                # Check permissions
                if not mirror.has_perm(self.current_user):
@@ -69,39 +88,50 @@ class MirrorEditHandler(base.BaseHandler):
        def post(self, hostname):
                mirror = self.backend.mirrors.get_by_hostname(hostname)
                if not mirror:
-                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+                       raise tornado.web.HTTPError(404, "Could not find mirror %s" % hostname)
 
                # Check permissions
                if not mirror.has_perm(self.current_user):
                        raise tornado.web.HTTPError(403)
 
                with self.db.transaction():
-                       mirror.hostname       = self.get_argument("name")
-                       mirror.path           = self.get_argument("path", "")
-                       mirror.owner          = self.get_argument("owner", None)
-                       mirror.contact        = self.get_argument("contact", None)
-                       mirror.supports_https = self.get_argument("supports_https", False)
+                       mirror.owner          = self.get_argument("owner")
+                       mirror.contact        = self.get_argument("contact")
+                       mirror.notes          = self.get_argument("notes", None)
 
-               self.redirect("/mirror/%s" % mirror.hostname)
+               self.redirect("/mirrors/%s" % mirror.hostname)
 
 
-class MirrorDeleteHandler(base.BaseHandler):
+class DeleteHandler(base.BaseHandler):
        @tornado.web.authenticated
        def get(self, hostname):
                mirror = self.backend.mirrors.get_by_hostname(hostname)
                if not mirror:
-                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+                       raise tornado.web.HTTPError(404, "Could not find mirror %s" % hostname)
+
+               # Check permissions
+               if not mirror.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403)
+
+               self.render("mirrors/delete.html", mirror=mirror)
+
+       @tornado.web.authenticated
+       def post(self, hostname):
+               mirror = self.backend.mirrors.get_by_hostname(hostname)
+               if not mirror:
+                       raise tornado.web.HTTPError(404, "Could not find mirror %s" % hostname)
 
                # Check permissions
                if not mirror.has_perm(self.current_user):
                        raise tornado.web.HTTPError(403)
 
-               confirmed = self.get_argument("confirmed", None)
-               if confirmed:
-                       with self.db.transaction():
-                               mirror.deleted = True
+               with self.db.transaction():
+                       mirror.delete(user=self.current_user)
 
-                       self.redirect("/mirrors")
-                       return
+               # Redirect back to all mirrors
+               self.redirect("/mirrors")
 
-               self.render("mirrors/delete.html", mirror=mirror)
+
+class ListModule(ui_modules.UIModule):
+       def render(self, mirrors):
+               return self.render_string("mirrors/modules/list.html", mirrors=mirrors)
index a9bae9c7799ac0a351b8869ac2316312f86ac982..e00f9c16c594dc04eba72d93861d57b29cb6a115 100644 (file)
@@ -173,7 +173,7 @@ class MirrorlistHandler(BaseHandler):
                mirrors = []
 
                # Fetch mirrors
-               for mirror in self.backend.mirrors.make_mirrorlist(self.current_address):
+               for mirror in self.backend.mirrors.get_mirrors_for_address(self.current_address):
                        mirrors.append({
                                "url"       : "/".join((mirror.url, repo.path, arch)),
                                "location"  : mirror.country_code,