try to remove dep to python3-postgresql that is no longer in f43 master
authorThierry Parmentelat <thierry.parmentelat@inria.fr>
Fri, 21 Nov 2025 10:35:49 +0000 (11:35 +0100)
committerThierry Parmentelat <thierry.parmentelat@inria.fr>
Fri, 21 Nov 2025 10:35:49 +0000 (11:35 +0100)
14 files changed:
.pylintrc [new file with mode: 0644]
PLC/Auth.py
PLC/Methods/AddPersonToSlice.py
PLC/Methods/DeletePersonFromSlice.py
PLC/Methods/DeleteSlice.py
PLC/Methods/GetPlcRelease.py
PLC/Methods/GetSlices.py
PLC/Persons.py
PLC/Slices.py
PLC/plc_crypt.py [new file with mode: 0644]
migrations/extract-views.py
planetlab5.sql
plc.d/db
plcapi.spec

diff --git a/.pylintrc b/.pylintrc
new file mode 100644 (file)
index 0000000..232be96
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,2 @@
+[FORMAT]
+max-line-length=110
index 927850e..5cea320 100644 (file)
@@ -5,7 +5,6 @@
 # Copyright (C) 2006 The Trustees of Princeton University
 #
 
-import crypt
 from hashlib import sha1 as sha
 import hmac
 import time
@@ -21,6 +20,7 @@ from PLC.Peers import Peer, Peers
 from PLC.Keys import Keys
 from PLC.Boot import notify_owners
 from PLC.Logger import logger
+from PLC.plc_crypt import plc_crypt
 
 class Auth(Parameter):
     """
@@ -319,7 +319,7 @@ class PasswordAuth(Auth):
 
             # Protect against blank passwords in the DB
             if password is None or password[:12] == "" or \
-               crypt.crypt(plaintext, password[:12]) != password:
+               plc_crypt(plaintext, password[:12]) != password:
                 raise PLCAuthenticationFailure(
                     "PasswordAuth: Password verification failed")
 
index 172e428..bdd3802 100644 (file)
@@ -56,6 +56,8 @@ class AddPersonToSlice(Method):
         # Logging variables
         self.event_objects = {'Person': [person['person_id']],
                               'Slice': [slice['slice_id']]}
+        self.message = 'Person %d added to slice %d' % \
+                          (person['person_id'], slice['slice_id'])
         self.object_ids = [slice['slice_id']]
 
         return 1
index e47ebbe..7b2ffe8 100644 (file)
@@ -55,5 +55,7 @@ class DeletePersonFromSlice(Method):
 
         self.event_objects = {'Slice': [slice['slice_id']],
                               'Person': [person['person_id']]}
+        self.message = 'Person %d deleted from slice %d' % \
+                          (person['person_id'], slice['slice_id'])
 
         return 1
index 49df6ea..f8bf332 100644 (file)
@@ -43,6 +43,8 @@ class DeleteSlice(Method):
                 raise PLCPermissionDenied("Specified slice not associated with any of your sites")
 
         slice.delete()
+
         self.event_objects = {'Slice': [slice['slice_id']]}
+        self.message = "Slice %d deleted" % slice['slice_id']
 
         return 1
index 07b16f8..99603fe 100644 (file)
@@ -4,12 +4,12 @@ from PLC.Faults import *
 
 import re
 
-comment_regexp = '\A\s*#.|\A\s*\Z|\Axxxxx'
+comment_regexp = r'\A\s*#.|\A\s*\Z|\Axxxxx'
 
-regexps = { 'build'   : '\A[bB]uild\s+(?P<key>[^:]+)\s*:\s*(?P<value>.*)\Z',
-            'tags'    : '\A(?P<key>[^:]+)\s*:=\s*(?P<value>.*)\Z',
+regexps = { 'build'   : r'\A[bB]uild\s+(?P<key>[^:]+)\s*:\s*(?P<value>.*)\Z',
+            'tags'    : r'\A(?P<key>[^:]+)\s*:=\s*(?P<value>.*)\Z',
 # spaces not part of key : ungreedy
-            'rpms'    : '\A(?P<key>[^:]+?)\s*::\s*(?P<value>.*)\Z',
+            'rpms'    : r'\A(?P<key>[^:]+?)\s*::\s*(?P<value>.*)\Z',
 }
 
 class GetPlcRelease(Method):
index fff2c39..5438aa0 100644 (file)
@@ -1,24 +1,45 @@
+# focusing on pylint, ignoring flake8
+# flake8: noqa
+
+# pylint: disable=arguments-differ
+# pylint: disable=redefined-builtin
+# pylint: disable=missing-docstring
+# pylint: disable=invalid-name
+
 from PLC.Method import Method
 from PLC.Parameter import Parameter, Mixed
 from PLC.Filter import Filter
 from PLC.Auth import Auth
-from PLC.Persons import Person, Persons
+from PLC.Persons import Person  # , Persons
 from PLC.Nodes import Nodes
-from PLC.Sites import Site, Sites
+from PLC.Sites import Sites  # , Site
 from PLC.Slices import Slice, Slices
 
 class GetSlices(Method):
     """
-    Returns an array of structs containing details about slices. If
-    slice_filter is specified and is an array of slice identifiers or
-    slice names, or a struct of slice attributes, only slices matching
-    the filter will be returned. If return_fields is specified, only the
-    specified details will be returned.
-
-    Users may only query slices of which they are members. PIs may
-    query any of the slices at their sites. Admins and nodes may query
-    any slice. If a slice that cannot be queried is specified in
-    slice_filter, details about that slice will not be returned.
+    Returns an array of structs containing details about slices. If slice_filter
+    is specified and is an array of slice identifiers or slice names, or a
+    struct of slice attributes, only slices matching the filter will be
+    returned. If return_fields is specified, only the specified details will be
+    returned.
+
+    Users may only query slices of which they are members. PIs may query any of
+    the slices at their sites. Admins and nodes may query any slice. If a slice
+    that cannot be queried is specified in slice_filter, details about that
+    slice will not be returned.
+
+    Note that there is a special treatment of expired slices; by default, they
+    are not returned. And there are two mechanisms at work here, one is to
+    filter out deleted slices, and the other is to filter out the ones that have
+    expired (slices may exist in a metastable state where they have expired but
+    are not yet deleted).
+
+    In order to overcome this behaviour, in addition to the usual filter mechanism,
+    you can
+    (1) use a dict filter and add the `DELETED` key - with any value, e.g. True -
+        that will return all slices even the ones that have been deleted
+    (2) also add a `EXPIRED` key to the dict filter - with any value, e.g. True -
+        to disable automatic filtering of expired slices.
     """
 
     roles = ['admin', 'pi', 'user', 'node']
@@ -35,7 +56,7 @@ class GetSlices(Method):
 
     returns = [Slice.fields]
 
-    def call(self, auth, slice_filter = None, return_fields = None):
+    def call(self, _auth, slice_filter = None, return_fields = None):
         # If we are not admin, make sure to return only viewable
         # slices.
         if isinstance(self.caller, Person) and \
@@ -66,7 +87,13 @@ class GetSlices(Method):
         else:
             added_fields = False
 
-        slices = Slices(self.api, slice_filter, return_fields)
+        # 2024 sept: we need a way to pass a None 'expires' argument to Slices
+        # which by default is now()
+        expires_kwd = {}
+        if isinstance(slice_filter, dict) and 'EXPIRED' in slice_filter:
+            expires_kwd = {'expires': None}
+            del slice_filter['EXPIRED']
+        slices = Slices(self.api, slice_filter, return_fields, **expires_kwd)
 
         # Filter out slices that are not viewable
         if isinstance(self.caller, Person) and \
index bc4b4c1..2239c50 100644 (file)
@@ -9,7 +9,6 @@ from hashlib import md5
 import time
 from random import Random
 import re
-import crypt
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter, Mixed
@@ -18,6 +17,7 @@ from PLC.Table import Row, Table
 from PLC.Roles import Role, Roles
 from PLC.Keys import Key, Keys
 from PLC.Messages import Message, Messages
+from PLC.plc_crypt import plc_crypt
 
 class Person(Row):
     """
@@ -78,7 +78,7 @@ class Person(Row):
         if not email:
             raise invalid_email
 
-        email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
+        email_re = re.compile(r'\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
         if not email_re.match(email):
             raise invalid_email
 
@@ -102,6 +102,7 @@ class Person(Row):
         database.
         """
 
+        # this for crypt.crypt means MD5
         magic = "$1$"
 
         if len(password) > len(magic) and \
@@ -111,7 +112,9 @@ class Person(Row):
             # Generate a somewhat unique 8 character salt string
             salt = str(time.time()) + str(Random().random())
             salt = md5(salt.encode()).hexdigest()[:8]
-            return crypt.crypt(password, magic + salt + "$")
+            # magic:3 salt:8 $:1 means the total salt is 12 characters
+            # as can be seen in Auth.py
+            return plc_crypt(password, magic + salt + "$")
 
     validate_date_created = Row.validate_timestamp
     validate_last_updated = Row.validate_timestamp
index 076d638..bcec360 100644 (file)
@@ -1,17 +1,28 @@
+# focusing on pylint, ignoring flake8
+# flake8: noqa
+
+# pylint: disable=invalid-name
+# pylint: disable=redefined-builtin
+# pylint: disable=unused-argument
+# pylint: disable=missing-function-docstring
+# pylint: disable=missing-module-docstring
+# pylint: disable=consider-using-f-string
+
 import time
 import re
 
-from PLC.Faults import *
+from PLC.Faults import PLCInvalidArgument
 from PLC.Parameter import Parameter, Mixed
 from PLC.Filter import Filter
-from PLC.Debug import profile
+from PLC.Debug import profile
 from PLC.Table import Row, Table
-from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
-from PLC.Nodes import Node
+from PLC.SliceInstantiations import SliceInstantiations  # , SliceInstantiation
+from PLC.Nodes import Node, Nodes
 from PLC.Persons import Person, Persons
-from PLC.SliceTags import SliceTag
+from PLC.SliceTags import SliceTag
 from PLC.Timestamp import Timestamp
 
+
 class Slice(Row):
     """
     Representation of a row in the slices table. To use, optionally
@@ -22,7 +33,7 @@ class Slice(Row):
 
     table_name = 'slices'
     primary_key = 'slice_id'
-    join_tables = ['slice_node', 'slice_person', 'slice_tag', 'peer_slice', 'node_slice_whitelist', 'leases']
+    join_tables = ['slice_node', 'slice_person', 'slice_tag', 'peer_slice', 'node_slice_whitelist', 'leases']
     fields = {
         'slice_id': Parameter(int, "Slice identifier"),
         'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
@@ -32,7 +43,8 @@ class Slice(Row):
         'description': Parameter(str, "Slice description", max = 2048, nullok = True),
         'max_nodes': Parameter(int, "Maximum number of nodes that can be assigned to this slice"),
         'creator_person_id': Parameter(int, "Identifier of the account that created this slice"),
-        'created': Parameter(int, "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
+        'created': Parameter(int,
+                             "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
         'expires': Parameter(int, "Date and time when slice expires, in seconds since UNIX epoch"),
         'node_ids': Parameter([int], "List of nodes in this slice", ro = True),
         'person_ids': Parameter([int], "List of accounts that can use this slice", ro = True),
@@ -47,7 +59,7 @@ class Slice(Row):
                         Parameter(str, "Fully qualified hostname"))]
         }
 
-    view_tags_name="view_slice_tags"
+    view_tags_name = "view_slice_tags"
     tags = {}
 
     def validate_name(self, name):
@@ -67,7 +79,7 @@ class Slice(Row):
         conflicts = Slices(self.api, [name])
         for slice in conflicts:
             if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
-                raise PLCInvalidArgument("Slice name already in use, %s"%name)
+                raise PLCInvalidArgument(f"Slice name already in use, {name}")
 
         return name
 
@@ -84,7 +96,7 @@ class Slice(Row):
         # N.B.: Responsibility of the caller to ensure that expires is
         # not too far into the future.
         check_future = not ('is_deleted' in self and self['is_deleted'])
-        return Timestamp.sql_validate( expires, check_future = check_future)
+        return Timestamp.sql_validate(expires, check_future=check_future)
 
     add_person = Row.add_object(Person, 'slice_person')
     remove_person = Row.remove_object(Person, 'slice_person')
@@ -101,6 +113,9 @@ class Slice(Row):
         Deletes persons not found in value list from this slice (using DeletePersonFromSlice).
         """
 
+        from PLC.Methods.AddPersonToSlice import AddPersonToSlice             # pylint: disable=import-outside-toplevel
+        from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice   # pylint: disable=import-outside-toplevel
+
         assert 'person_ids' in self
         assert 'slice_id' in self
         assert isinstance(value, list)
@@ -114,15 +129,14 @@ class Slice(Row):
 
         # Add new ids, remove stale ids
         if self['person_ids'] != person_ids:
-            from PLC.Methods.AddPersonToSlice import AddPersonToSlice
-            from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
             new_persons = set(person_ids).difference(self['person_ids'])
             stale_persons = set(self['person_ids']).difference(person_ids)
 
             for new_person in new_persons:
                 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, new_person, self['slice_id'])
             for stale_person in stale_persons:
-                DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, stale_person, self['slice_id'])
+                DeletePersonFromSlice.__call__(
+                    DeletePersonFromSlice(self.api), auth, stale_person, self['slice_id'])
 
     def associate_nodes(self, auth, field, value):
         """
@@ -130,7 +144,8 @@ class Slice(Row):
         Deletes nodes not found in value list from this slice (using DeleteSliceFromNodes).
         """
 
-        from PLC.Nodes import Nodes
+        from PLC.Methods.AddSliceToNodes import AddSliceToNodes             # pylint: disable=import-outside-toplevel
+        from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes   # pylint: disable=import-outside-toplevel
 
         assert 'node_ids' in self
         assert 'slice_id' in self
@@ -145,15 +160,16 @@ class Slice(Row):
 
         # Add new ids, remove stale ids
         if self['node_ids'] != node_ids:
-            from PLC.Methods.AddSliceToNodes import AddSliceToNodes
-            from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
             new_nodes = set(node_ids).difference(self['node_ids'])
             stale_nodes = set(self['node_ids']).difference(node_ids)
 
             if new_nodes:
-                AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, self['slice_id'], list(new_nodes))
+                AddSliceToNodes.__call__(
+                    AddSliceToNodes(self.api), auth, self['slice_id'], list(new_nodes))
             if stale_nodes:
-                DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, self['slice_id'], list(stale_nodes))
+                DeleteSliceFromNodes.__call__(
+                    DeleteSliceFromNodes(self.api), auth, self['slice_id'], list(stale_nodes))
+
     def associate_slice_tags(self, auth, fields, value):
         """
         Deletes slice_tag_ids not found in value list (using DeleteSliceTag).
@@ -161,16 +177,17 @@ class Slice(Row):
         Updates slice_tag if slice_fields w/ slice_id is found (using UpdateSlceiAttribute).
         """
 
+        from PLC.Methods.DeleteSliceTag import DeleteSliceTag           # pylint: disable=import-outside-toplevel
+
         assert 'slice_tag_ids' in self
         assert isinstance(value, list)
 
-        (attribute_ids, blank, attributes) = self.separate_types(value)
+        (attribute_ids, _, attributes) = self.separate_types(value)
 
         # There is no way to add attributes by id. They are
         # associated with a slice when they are created.
         # So we are only looking to delete here
         if self['slice_tag_ids'] != attribute_ids:
-            from PLC.Methods.DeleteSliceTag import DeleteSliceTag
             stale_attributes = set(self['slice_tag_ids']).difference(attribute_ids)
 
             for stale_attribute in stale_attributes:
@@ -179,9 +196,8 @@ class Slice(Row):
         # If dictionary exists, we are either adding new
         # attributes or updating existing ones.
         if attributes:
-            from PLC.Methods.AddSliceTag import AddSliceTag
-            from PLC.Methods.UpdateSliceTag import UpdateSliceTag
-
+            from PLC.Methods.AddSliceTag import AddSliceTag             # pylint: disable=import-outside-toplevel
+            from PLC.Methods.UpdateSliceTag import UpdateSliceTag       # pylint: disable=import-outside-toplevel
             added_attributes = [x for x in attributes if 'slice_tag_id' not in x]
             updated_attributes = [x for x in attributes if 'slice_tag_id' in x]
 
@@ -208,7 +224,8 @@ class Slice(Row):
                 else:
                     nodegroup_id = None
 
-                AddSliceTag.__call__(AddSliceTag(self.api), auth, self['slice_id'], type, value, node_id, nodegroup_id)
+                AddSliceTag.__call__(
+                    AddSliceTag(self.api), auth, self['slice_id'], type, value, node_id, nodegroup_id)
             for updated_attribute in updated_attributes:
                 attribute_id = updated_attribute.pop('slice_tag_id')
                 if attribute_id not in self['slice_tag_ids']:
@@ -216,7 +233,7 @@ class Slice(Row):
                 else:
                     UpdateSliceTag.__call__(UpdateSliceTag(self.api), auth, attribute_id, updated_attribute)
 
-    def sync(self, commit = True):
+    def sync(self, commit = True):           # pylint: disable=arguments-differ
         """
         Add or update a slice.
         """
@@ -237,9 +254,11 @@ class Slice(Row):
         assert 'slice_id' in self
 
         # Clean up miscellaneous join tables
-        for table in self.join_tables:
-            self.api.db.do("DELETE FROM %s WHERE slice_id = %d" % \
-                           (table, self['slice_id']))
+        # however, we want to preserve leases as well as slice tags for accounting purposes
+        to_delete = [table for table in self.join_tables if table not in {'leases', 'slice_tag'}]
+        for table in to_delete:
+            self.api.db.do(
+                f"DELETE FROM {table} WHERE slice_id = {self['slice_id']}")
 
         # Mark as deleted
         self['is_deleted'] = True
@@ -259,18 +278,19 @@ class Slices(Table):
         view = "view_slices"
         # as many left joins as requested tags
         for tagname in self.tag_columns:
-            view= "%s left join %s using (%s)"%(view,Slice.tagvalue_view_name(tagname),
-                                                Slice.primary_key)
+            table = Slice.tagvalue_view_name(tagname)
+            view = f"{view} left join {table} using ({Slice.primary_key})"
 
-        sql = "SELECT %s FROM %s WHERE is_deleted IS False" % \
-              (", ".join(list(self.columns.keys())+list(self.tag_columns.keys())),view)
+        selected = ", ".join(list(self.columns.keys())+list(self.tag_columns.keys()))
+        sql = f"SELECT {selected} FROM {view} WHERE TRUE"
+        exclude_deleted = True
 
         if expires is not None:
             if expires >= 0:
-                sql += " AND expires > %d" % expires
+                sql += f" AND expires > {expires}"
             else:
                 expires = -expires
-                sql += " AND expires < %d" % expires
+                sql += f" AND expires < {expires}"
 
         if slice_filter is not None:
             if isinstance(slice_filter, (list, tuple, set)):
@@ -280,16 +300,22 @@ class Slices(Table):
                 slice_filter = Filter(Slice.fields, {'slice_id': ints, 'name': strs})
                 sql += " AND (%s) %s" % slice_filter.sql(api, "OR")
             elif isinstance(slice_filter, dict):
-                allowed_fields=dict(list(Slice.fields.items())+list(Slice.tags.items()))
+                if 'DELETED' in slice_filter:
+                    exclude_deleted = False
+                    del slice_filter['DELETED']
+                allowed_fields = dict(list(Slice.fields.items())+list(Slice.tags.items()))
                 slice_filter = Filter(allowed_fields, slice_filter)
                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
-            elif isinstance (slice_filter, str):
-                slice_filter = Filter(Slice.fields, {'name':slice_filter})
+            elif isinstance(slice_filter, str):
+                slice_filter = Filter(Slice.fields, {'name': slice_filter})
                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
-            elif isinstance (slice_filter, int):
-                slice_filter = Filter(Slice.fields, {'slice_id':slice_filter})
+            elif isinstance(slice_filter, int):
+                slice_filter = Filter(Slice.fields, {'slice_id': slice_filter})
                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
             else:
-                raise PLCInvalidArgument("Wrong slice filter %r"%slice_filter)
+                raise PLCInvalidArgument(f"Wrong slice filter {slice_filter!r}")
+
+        if exclude_deleted:
+            sql += " AND is_deleted IS False"
 
         self.selectall(sql)
diff --git a/PLC/plc_crypt.py b/PLC/plc_crypt.py
new file mode 100644 (file)
index 0000000..2715495
--- /dev/null
@@ -0,0 +1,149 @@
+"""
+compatibility replacement for our needs among the old crypt module
+that was removed in 3.13 (failed to see the earlier warnings...)
+
+were only using the crypt.crypt() function
+plus, we only use MD5 for hashing, and we always provide our salt
+so we only need to replace that
+
+this is stolen from (and a thousands thanks to) the author of this code:
+https://github.com/guffre/python-crypt/blob/main/crypt.py
+"""
+
+import base64
+import hashlib
+
+
+def crypt_base64(buffer):
+    """
+    The custom base64 that is specific to these crypt algorithms
+    I know it looks weird, but this is apparently what the spec is
+    """
+    unix_crypt_base = (
+        b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+    )
+    normal_base = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+    ret = b""
+    if len(buffer) == 16:
+        c1 = list(range(0, 5))
+        c2 = list(range(6, 11))
+        c3 = list(range(12, 16)) + [5]
+    elif len(buffer) == 32:
+        c1 = [(n * 21 + 00) % 30 for n in range(10)]
+        c2 = [(n * 21 + 10) % 30 for n in range(10)]
+        c3 = [(n * 21 + 20) % 30 for n in range(10)]
+    elif len(buffer) == 64:
+        c1 = [(n * 22 + 00) % 63 for n in range(21)]
+        c2 = [(n * 22 + 21) % 63 for n in range(21)]
+        c3 = [(n * 22 + 42) % 63 for n in range(21)]
+    else:
+        return None
+    for block in zip(c1, c2, c3):
+        collect = b"".join(buffer[n : n + 1] for n in block)
+        ret += base64.b64encode(collect)[::-1]
+    if len(buffer) == 16:
+        ret += base64.b64encode(b"00" + buffer[11:12])[::-1][:2]
+    elif len(buffer) == 32:
+        ret += base64.b64encode(b"0" + buffer[31:32] + buffer[30:31])[::-1][:2]
+    elif len(buffer) == 64:
+        ret += base64.b64encode(b"00" + buffer[63:64])[::-1][:2]
+    # Python 2/3 compatability
+    trans = string if bytes == str else bytes
+    ret = ret.translate(trans.maketrans(normal_base, unix_crypt_base))
+    return ret
+
+
+def bin_crypt_md5(key:bytes, salt:bytes) -> bytes:
+
+    salt = salt[:8]
+    rounds = 1000
+
+    # Initialize contexts
+    ctx = hashlib.md5(key + b"$1$" + salt)
+    alt_ctx = hashlib.md5(key + salt + key)
+    alt_result = alt_ctx.digest()
+
+    # Add hash-bytes of second context to first
+    cnt = len(key)
+    while cnt > ctx.digest_size:
+        ctx.update(alt_result)
+        cnt -= ctx.digest_size
+    ctx.update(alt_result[:cnt])
+
+    # Binary mix
+    cnt = len(key)
+    while cnt > 0:
+        if (cnt & 1) != 0:
+            ctx.update(b"\0")
+        else:
+            ctx.update(key[0:1])
+        cnt = cnt >> 1
+
+    alt_result = ctx.digest()
+
+    # Perform rounds of hashing
+    for cnt in range(rounds):
+        ctx = hashlib.md5()
+        if (cnt & 1) != 0:
+            ctx.update(key)
+        else:
+            ctx.update(alt_result)
+        if cnt % 3 != 0:
+            ctx.update(salt)
+        if cnt % 7 != 0:
+            ctx.update(key)
+        if (cnt & 1) != 0:
+            ctx.update(alt_result)
+        else:
+            ctx.update(key)
+        alt_result = ctx.digest()
+
+    # Crypt-base64 the hash digest
+    encoded = crypt_base64(alt_result).decode()
+
+    # Make some nice output
+    hashnumber = 1
+    formatted = "${}${}${}".format(hashnumber, salt.decode(), encoded)
+    return (alt_result, formatted)
+
+
+def crypt_md5(password: str, salt: str) -> str:
+    """
+    same as bin_crypt_md5 but with strings
+    """
+    key = password.encode()
+    salt = salt[3:].encode()
+    return bin_crypt_md5(key, salt)[1]
+
+plc_crypt = crypt_md5
+
+
+# some test cases
+def test_crypt():
+    # these were produced by the crypt.crypt() function
+    BY_STANDARD_CRYPT = [
+        ["password",         "$1$6167a424$9DuVAlIQ8zHRnth8ZUVRk1"],
+        ["password",         "$1$d2b53fd1$nhHpCadNhArACvELo1Ugm0"],
+        ["password1",        "$1$ab75e533$MXdWjzcZ9HR3B5x8uBCkC."],
+        ["password1",        "$1$38520235$sRIMkorcm235lA0V6IKdN1"],
+        ["password123",      "$1$5012ee70$a.557l7kfK/ksQ5dinvIW/"],
+        ["password123",      "$1$f76ad85d$bli2s3/GQ8C5452km3Bdd."],
+        ["tarabiscota",      "$1$b426146a$IkJK0vYHkWGV4KHDJ7wKw/"],
+        ["tarabiscota",      "$1$9bd6a92f$/uObTeHg3wlDwJB7YO8DZ."],
+        ["56890&^*(HGJkjh",  "$1$b5bf816d$HqfMqyXloPHo3mabIiSAt1"],
+        ["56890&^*(HGJkjh",  "$1$a9c16150$n0yY5WatF7noVf8wyTHR51"],
+        [ "a2357A2357", "$1$e846962a$L/RA8n/sWWyvaE7YsjKOp0" ],
+        [ "hello-world", "$1$91ed0406$g2THz.r6/4Zhu3EHZzpfW." ],
+        [ "sodfisj0395u023-0=-a]\\df[skg]", "$1$e51c6cb6$rj.okkN6wwLR8djoiElK3." ],
+        [ "`)(*678(*(67HGJ)))", "$1$762b471f$tUY9KN9QsngZ4hpuEuyWo1" ],
+    ]
+
+    for plaintext_password, expected in BY_STANDARD_CRYPT:
+        computed = plc_crypt(plaintext_password, expected)
+        if computed == expected:
+            print(f"match: {plaintext_password:>30} == {computed}")
+        else:
+            print(f"ERROR: {plaintext_password:>30}: {computed} != {expected} !!!")
+
+if __name__ == "__main__":
+    test_crypt()
index 4a20465..59089e1 100755 (executable)
@@ -10,9 +10,9 @@ class Schema:
         self.output=output
 
     # left part is non-greedy
-    comment = re.compile("(.*?)--.*")
-    spaces = re.compile("^\s+(\S.*)")
-    view = re.compile("(?i)\s*create\s+(or\s+replace)?\s+view.*")
+    comment = re.compile(r"(.*?)--.*")
+    spaces = re.compile(r"^\s+(\S.*)")
+    view = re.compile(r"(?i)\s*create\s+(or\s+replace)?\s+view.*")
 
     def parse (self):
         if self.output:
index 5478836..4c20ce8 100644 (file)
@@ -768,7 +768,7 @@ CREATE OR REPLACE VIEW site_slices AS
 SELECT site_id,
 array_accum(slice_id) AS slice_ids
 FROM slices
-WHERE is_deleted is false
+WHERE is_deleted is false and expires > CURRENT_TIMESTAMP
 GROUP BY site_id;
 
 -- Slice membership
index 4d0ef8a..66c173e 100755 (executable)
--- a/plc.d/db
+++ b/plc.d/db
@@ -21,6 +21,7 @@ export PGPORT=$PLC_DB_PORT
 # Install extensions
 function extend_db() {
        shopt -s nullglob
+       local script name extension version
        for file in /usr/share/plc_api/extensions/*-up*; do
                script=${file##*/}
                name=${script%-up*}
@@ -69,10 +70,11 @@ function extend_db() {
 # current subversion. At least one of the migration scripts with the
 # same N must update plc_db_version.subversion.
 function migrate_db() {
-       subversion=$(psql -U $PLC_DB_USER --quiet --tuples-only --no-align -c \
+       local subversion=$(psql -U $PLC_DB_USER --quiet --tuples-only --no-align -c \
                "SELECT subversion FROM plc_db_version LIMIT 1" \
                $PLC_DB_NAME 2>/dev/null || echo 0)
        shopt -s nullglob
+       local file script index extension
        for file in /usr/share/plc_api/migrations/[0-9]*-up-*; do
                script=$(basename $file)
                index=${script%-up*}
@@ -95,49 +97,55 @@ function migrate_db() {
 }
 
 function checkpoint_planetlab_db() {
-       dumpfile=$1
        pg_dump -U $PLC_DB_USER $PLC_DB_NAME >$dumpfile
+       local dumpfile="$1"; shift
        check
 }
 
-function restore_planetlab_db() {
-       dumpfile=$1
-       if [ -n "$dumpfile" ]; then
-               [ -f "$dumpfile" ] && psql -a -U $PLC_DB_USER $PLC_DB_NAME <$dumpfile
-               check
-       fi
-}
-
 # use a single date of this script invocation for the dump_*_db functions.
 DATE=$(date +"%Y-%m-%d-%H-%M-%S")
 
 # Dumps the database - optional argument to specify filename suffix
 function dump_planetlab_db() {
-       if [ -n "$1" ]; then suffix="-$1"; else suffix=""; fi
+       if [ -n "$1" ]; then
+               # avoid ending with .sql.sql
+               suffix=$(basename -- "$1" .sql)
+               suffix="-$suffix"
+       else
+               suffix=""
+       fi
        dumpfile=/var/lib/pgsql/backups/$(date +"${PLC_DB_NAME}.${DATE}${suffix}.sql")
        checkpoint_planetlab_db $dumpfile
 }
 
-function restore_drupal_db() {
-       dumpfile=$1
-       if [ -n "$dumpfile" ]; then
-               [ -f "$dumpfile" ] && psql -a -U $PLC_DB_USER drupal <$1
-               check
-       fi
-}
-
 function checkpoint_drupal_db() {
-       dumpfile=$1
+       local dumpfile="$1"; shift
        pg_dump -U $PLC_DB_USER drupal >$dumpfile
        check
 }
 
 function dump_drupal_db() {
-       dumpfile=/var/lib/pgsql/backups/$(date +"drupal.${DATE}.sql")
+       local dumpfile=/var/lib/pgsql/backups/$(date +"drupal.${DATE}.sql")
        checkpoint_drupal_db $dumpfile
        check
 }
 
+function restore_planetlab_db() {
+       local dumpfile="$1"; shift
+       [[ -z "$dumpfile" ]] && { echo "Usage: $0 restore (planetlab5) <dumpfile>"; return 1; }
+       psql -U postgres -c "DROP DATABASE $PLC_DB_NAME"
+       createdb -U postgres --template=template0 --encoding=UNICODE --owner=$PLC_DB_USER $PLC_DB_NAME
+       psql -a -U $PLC_DB_USER $PLC_DB_NAME -f $dumpfile
+}
+
+function restore_drupal_db() {
+       local dumpfile="$1"; shift
+       [[ -z "$dumpfile" ]] && { echo "Usage: $0 restore (drupal) <dumpfile>"; return 1; }
+       psql -U postgres -c "DROP DATABASE drupal"
+       createdb -U postgres --template=template0 --encoding=UNICODE --owner=$PLC_DB_USER drupal
+       psql -a -U $PLC_DB_USER drupal -f $dumpfile
+}
+
 # Clean up old backups
 function clean_dump() {
        local days="$1"; shift
index a3e1aea..68e0ce9 100644 (file)
@@ -1,6 +1,6 @@
 %define name plcapi
-%define version 7.1
-%define taglevel 1
+%define version 7.2
+%define taglevel 7
 
 %define release %{taglevel}%{?pldistro:.%{pldistro}}%{?date:.%{date}}
 
@@ -28,7 +28,7 @@ Requires: httpd mod_ssl
 Requires: postgresql, postgresql-server
 # We use set everywhere
 Requires: python3
-Requires: python3-postgresql
+#Requires: python3-postgresql
 Requires: python3-psycopg2
 Requires: python3-pycurl
 # used in GPG.py as a replacement to PyXML's Canonicalize
@@ -127,7 +127,35 @@ rm -rf $RPM_BUILD_ROOT
 
 
 %changelog
-* Tue Feb 06 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.1-0
+* Thu Apr 24 2025 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.2-7
+- change site_slices view to exclude expired slices
+  that are now lingering i.e. not marked as deleted
+
+* Mon Nov 25 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.2-6
+- comes with its own crypt function, as a replacement to the standard crypt
+  that is longer part of python-3.13
+- more robust toolset to dump/restore the databases
+- do not use 7.2-5 that is broken
+
+* Tue Oct 15 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.2-4
+- when deleting a slice, do not remove corresponding tags
+
+* Mon Oct 14 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.2-3
+- when deleting a slice, do not remove corresponding leases
+- the DeleteSlice method gets logged with an event
+
+* Mon Sep 24 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.2-2
+- AddPersonToSlice and DeletePersonFromSlice get logged with an event
+
+* Mon Sep 24 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.2-1
+- GetSlices({'EXPIRED': True, 'DELETED': True}) are now allowed to retrieve,
+  respectively, expired and deleted slices
+
+* Wed Feb 07 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.1-2
+- fix clipping of GetLeases, that was having corner cases
+  with adjacent timeslots
+
+* Tue Feb 06 2024 Thierry Parmentelat <thierry.parmentelat@inria.fr> - plcapi-7.1-1
 - allow GetLeases 'clip' and 'alive' parameters to use lists
   as tuples cannot be marshalled by the xmlrpc layer
 - packaging modified for f37