--- /dev/null
+[FORMAT]
+max-line-length=110
# Copyright (C) 2006 The Trustees of Princeton University
#
-import crypt
from hashlib import sha1 as sha
import hmac
import time
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):
"""
# 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")
# 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
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
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
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):
+# 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']
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 \
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 \
import time
from random import Random
import re
-import crypt
from PLC.Faults import *
from PLC.Parameter import Parameter, Mixed
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):
"""
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
database.
"""
+ # this for crypt.crypt means MD5
magic = "$1$"
if len(password) > len(magic) and \
# 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
+# 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
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"),
'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),
Parameter(str, "Fully qualified hostname"))]
}
- view_tags_name="view_slice_tags"
+ view_tags_name = "view_slice_tags"
tags = {}
def validate_name(self, name):
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
# 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')
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)
# 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):
"""
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
# 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).
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:
# 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]
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']:
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.
"""
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
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)):
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)
--- /dev/null
+"""
+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()
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:
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
# 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*}
# 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*}
}
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
%define name plcapi
-%define version 7.1
-%define taglevel 1
+%define version 7.2
+%define taglevel 7
%define release %{taglevel}%{?pldistro:.%{pldistro}}%{?date:.%{date}}
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
%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