summaryrefslogblamecommitdiffstats
path: root/src/config-db/OpenSLX/DBSchema.pm
blob: d3e7573bdd95f9e7aec41f907c946859034fbcdf (plain) (tree)
1
2
3
4
5
6
7
8
                                         
 

                                                                    
 

                                                                         
 


                                                                               
                                                         
                                                                               
                          

           

             
                    


                                                                                











                                                                                 

                                                                                
                   

                





                                                  
                                                                                   
                                              

                                                                             





                                                                                






                                                 


                                                           




                                                                            

                                                 










                                                                                 

                                                                             
                                                       
                                                                          
                                                              
                                                                              









                                                                               

                                           














                                                                                

                                                                            
                                                                   
                                                                             





                                                 


                                                          




                                               

                                                 





                                                                          

                                                 






                                                                         

                                                              










                                                                              

                                                     




                                                    




                                                                               


                       

                                                   







                                                                          











                                                                                 




                                              







                                                                             


                                                           











                                                                              

                                                                                



                                                                                


                                                                                
   




                                                                                
                      
 

                
 
                               



                                      

                         
 
                                        
 








                                                                               
 










                                                                               

                                                









                                                                      

                                                



                                                                             
 
             



                     

                          
 


                                                     




                                                                                

                                                                                

                    

                        


                            
 

                                                                    
 




                                                        
 
             
 
 
                    

































                                                                 
                                     







































































                                                             
                                     


































































                                                           
                                     





























































                                                                            
 















































                                                                              
 



















































                                                                               




                                                                           


















                                              








                                                                            







                                                                            







                                                                             

















                                              




















































                                                                           













                                             
















                                                                         
  
 
  
# Copyright (c) 2006, 2007 - OpenSLX GmbH
#
# This program is free software distributed under the GPL version 2.
# See http://openslx.org/COPYING
#
# If you have any feedback please consult http://openslx.org/feedback and
# send your suggestions, praise, or complaints to feedback@openslx.org
#
# General information about OpenSLX can be found at http://openslx.org/
# -----------------------------------------------------------------------------
# DBSchema.pm
#    - provides database schema of the OpenSLX config-db.
# -----------------------------------------------------------------------------
package OpenSLX::DBSchema;

use strict;
use warnings;

use OpenSLX::Basics;

################################################################################
### DB-schema definition
###     This hash-ref describes the current OpenSLX configuration database 
###        schema.
###        Each table is defined by a list of column descriptions (and optionally
###        a list of default values).
###     A column description is simply the name of the column followed by ':'
###     followed by the data type description. The following data types are
###     currently supported:
###         b        => boolean (providing the values 1 and 0 only)
###         i        => integer (32-bit, signed)
###         s.20    => string, followed by length argument (in this case: 20)
###         pk        => primary key (integer)
###         fk        => foreign key (integer)
################################################################################

my $VERSION = 0.36;

my $DbSchema = {
    'version' => $VERSION,
    'tables' => {
        'client' => {
            # a client is a PC booting via network
            'cols' => [
                'id:pk',            # primary key
                'name:s.128',       # official name of PC (e.g. as given by sticker
                                    # on case)
                'mac:s.20',         # MAC of NIC used for booting
                'comment:s.1024',   # internal comment (optional, for admins)
            ],
            'vals' => [
                {   # add default client
                    'id'         => 0,
                    'name'       => '<<<default>>>',
                    'comment'    => 'internal client that holds default values',
                },
            ],
        },
        'client_attr' => {
            # attributes of clients
            'cols' => [
                'id:pk',            # primary key
                'client_id:fk',     # foreign key to client
                'name:s.128',       # attribute name
                'value:s.255',      # attribute value
            ],
        },
        'client_system_ref' => {
            # clients referring to the systems they should offer for booting
            'cols' => [
                'client_id:fk',     # foreign key
                'system_id:fk',     # foreign key
            ],
        },
        'export' => {
            # an export describes a vendor-OS "wrapped" in some kind of exporting
            # format (NFS or NBD-squash). This represents the rootfs that the
            # clients will see.
            'cols' => [
                'id:pk',            # primary key
                'name:s.64',        # unique name of export, is automatically
                                    # constructed like this:
                                    #   <vendor-os-name>-<export-type>
                'vendor_os_id:fk',  # foreign key
                'comment:s.1024',   # internal comment (optional, for admins)
                'type:s.10',        # 'nbd', 'nfs', ...
                'server_ip:s.16',   # IP of exporting server, if empty the
                                    # boot server will be used
                'port:i',           # some export types need to use a specific
                                    # port for each incarnation, if that's the
                                    # case you can specify it here
                'uri:s.255',        # path to export (squashfs or NFS-path), if
                                    # empty it will be auto-generated by
                                    # config-demuxer
            ],
        },
        'global_info' => {
            # a home for global counters and other info
            'cols' => [
                'id:s.32',          # key
                'value:s.128',      # value
            ],
            'vals' => [
                {   # add nbd-server-port
                    'id' => 'next-nbd-server-port',
                    'value' => '5000',
                },
            ],
        },
        'groups' => {
            # a group encapsulates a set of clients as one entity, managing
            # a group-specific attribute set. All the different attribute
            # sets a client inherits via group membership are folded into
            # one resulting attribute set with respect to each group's priority.
            'cols' => [
                'id:pk',            # primary key
                'name:s.128',       # name of group
                'priority:i',       # priority, used for order in group-list
                                    # (from 0-highest to 99-lowest)
                'comment:s.1024',   # internal comment (optional, for admins)
            ],
        },
        'group_attr' => {
            # attributes of groups
            'cols' => [
                'id:pk',            # primary key
                'group_id:fk',      # foreign key to group
                'name:s.128',       # attribute name
                'value:s.255',      # attribute value
            ],
        },
        'group_client_ref' => {
            # groups referring to their clients
            'cols' => [
                'group_id:fk',      # foreign key
                'client_id:fk',     # foreign key
            ],
        },
        'group_system_ref' => {
            # groups referring to the systems each of their clients should
            # offer for booting
            'cols' => [
                'group_id:fk',      # foreign key
                'system_id:fk',     # foreign key
            ],
        },
        'installed_plugin' => {
            # holds the plugins that have been installed into a specific 
            # vendor-OS
            'cols' => [
                'id:pk',            # primary key
                'vendor_os_id:fk',  # foreign key
                'plugin_name:s.64', # name of installed plugin
                                    # (e.g. suse-9.3-kde, debian-3.1-ppc,
                                    # suse-10.2-cloned-from-kiwi).
                                    # This is used as the folder name for the
                                    # corresponding stage1, too.
            ],
        },
        'installed_plugin_attr' => {
            # (stage1-)attributes of installed plugins
            'cols' => [
                'id:pk',            # primary key
                'installed_plugin_id:fk',    # foreign key to installed plugin
                'name:s.128',       # attribute name
                'value:s.255',      # attribute value
            ],
        },
        'meta' => {
            # information about the database as such
            'cols' => [
                'plugin_info_hash:s.32',    # hash-value identifying a specific
                                            # set of plugins and their 
                                            # attributes
                'schema_version:s.5',       # schema-version currently 
                                            # implemented by DB
            ],
            'vals' => [
                {
                    'plugin_info_hash' => '',
                    'schema_version'   => $VERSION,
                },
            ],
        },
        'system' => {
            # a system describes one bootable instance of an export, it
            # represents a selectable line in the PXE boot menu of all the
            # clients associated with this system
            'cols' => [
                'id:pk',              # primary key
                'export_id:fk',       # foreign key
                'name:s.64',          # unique name of system, is automatically
                                      # constructed like this:
                                      #   <vendor-os-name>-<export-type>-<kernel>
                'label:s.64',         # name visible to user (pxe-label)
                                      # if empty, this will be autocreated from
                                      # the name
                'kernel:s.128',       # path to kernel file, relative to /boot
                'description:s.512',  # visible description (for PXE TEXT)
                'pxe_prefix_ip:s.16', # ip prefix for PXE Menu entry
                'comment:s.1024',     # internal comment (optional, for admins)
            ],
            'vals' => [
                {   # add default system
                    'id' => 0,
                    'name' => '<<<default>>>',
                    'comment' => 'internal system that holds default values',
                },
            ],
        },
        'system_attr' => {
            # attributes of systems
            'cols' => [
                'id:pk',            # primary key
                'system_id:fk',     # foreign key to system
                'name:s.128',       # attribute name
                'value:s.255',      # attribute value
            ],
        },
        'vendor_os' => {
            # a vendor-OS describes a folder containing an operating system as
            # provided by the vendor (a.k.a. unchanged and thus updatable)
            'cols' => [
                'id:pk',            # primary key
                'name:s.48',        # structured name of OS installation
                                    # (e.g. suse-9.3-kde, debian-3.1-ppc,
                                    # suse-10.2-cloned-from-kiwi).
                                    # This is used as the folder name for the
                                    # corresponding stage1, too.
                'comment:s.1024',   # internal comment (optional, for admins)
                'clone_source:s.255',   # if vendor-OS was cloned, this contains
                                        # the rsync-URI pointing to the original
            ],
        },
    },
};

################################################################################
###
### standard methods
###
################################################################################
sub new
{
    my $class = shift;

    my $self = {
    };

    return bless $self, $class;
}

sub checkAndUpgradeDBSchemaIfNecessary
{
    my $self     = shift;
    my $configDB = shift;

    my $metaDB = $configDB->{'meta-db'};

    vlog(2, "trying to determine schema version...");
    my $currVersion = $metaDB->schemaFetchDBVersion();
    if (!defined $currVersion) {
        # that's bad, someone has messed with our DB: there is a
        # database, but the 'meta'-table is empty. 
        # There might still be data in the other tables, but we have no way to 
        # find out which schema version they're in. So it's safer to give up.
        croak _tr('Could not determine schema version of database');
    }

    if ($currVersion == 0) {
        vlog(1, _tr('Creating DB (schema version: %s)', $DbSchema->{version}));
        foreach my $tableName (keys %{$DbSchema->{tables}}) {
            # create table (optionally inserting default values, too)
            $metaDB->schemaAddTable(
                $tableName,
                $DbSchema->{tables}->{$tableName}->{cols},
                $DbSchema->{tables}->{$tableName}->{vals}
            );
        }
        $metaDB->schemaSetDBVersion($DbSchema->{version});
        $configDB->cleanupAnyInconsistencies() 
            or die _tr('unable to cleanup DB!');
        vlog(1, _tr('DB has been created successfully'));
    } elsif ($currVersion < $DbSchema->{version}) {
        vlog(
            1,
            _tr(
                'Our schema-version is %s, DB is %s, upgrading DB...',
                $DbSchema->{version}, $currVersion
            )
        );
        $self->_schemaUpgradeDBFrom($metaDB, $currVersion);
        $configDB->cleanupAnyInconsistencies() 
            or die _tr('unable to cleanup DB!');
        vlog(1, _tr('upgrade done'));
    } else {
        vlog(1, _tr('DB matches current schema version (%s)', $currVersion));
    }

    return 1;
}

sub getColumnsOfTable
{
    my $self      = shift;
    my $tableName = shift;

    return    
        map { (/^(\w+)\W/) ? $1 : $_; } 
        @{$DbSchema->{tables}->{$tableName}->{cols}};
}

################################################################################
###
### methods for upgrading the DB schema
###
################################################################################
my %DbSchemaHistory;

sub _schemaUpgradeDBFrom
{
    my $self        = shift;
    my $metaDB      = shift;
    my $currVersion = shift;

    foreach my $version (sort { $a <=> $b } keys %DbSchemaHistory) {
        next if $currVersion >= $version;

        vlog(0, "upgrading schema version to $version");
        if ($DbSchemaHistory{$version}->($metaDB)) {
            $metaDB->schemaSetDBVersion($version);
        }
    }

    return 1;
}

%DbSchemaHistory = (
    0.2 => sub {
        my $metaDB = shift;
    
        # move attributes into separate tables ...
        #
        # ... system attributes ...
        $metaDB->schemaAddTable(
            'system_attr', 
            [
                'id:pk',
                'system_id:fk',
                'name:s.128',
                'value:s.255',
            ]
        );
        foreach my $system ($metaDB->fetchSystemByFilter()) {
            my %attrs;
            foreach my $key (keys %$system) {
                next if substr($key, 0, 5) ne 'attr_';
                my $attrValue = $system->{$key} || '';
                next if $system->{id} > 0 && !length($attrValue);
                my $newAttrName = substr($key, 5);
                $attrs{$newAttrName} = $attrValue;
            }
            $metaDB->setSystemAttrs($system->{id}, \%attrs);
        }
        $metaDB->schemaDropColumns(
            'system',
            [
                'attr_automnt_dir',
                'attr_automnt_src',
                'attr_country',
                'attr_dm_allow_shutdown',
                'attr_hw_graphic',
                'attr_hw_local_disk',
                'attr_hw_monitor',
                'attr_hw_mouse',
                'attr_late_dm',
                'attr_netbios_workgroup',
                'attr_nis_domain',
                'attr_nis_servers',
                'attr_ramfs_fsmods',
                'attr_ramfs_miscmods',
                'attr_ramfs_nicmods',
                'attr_ramfs_screen',
                'attr_sane_scanner',
                'attr_scratch',
                'attr_slxgrp',
                'attr_start_alsasound',
                'attr_start_atd',
                'attr_start_cron',
                'attr_start_dreshal',
                'attr_start_ntp',
                'attr_start_nfsv4',
                'attr_start_printer',
                'attr_start_samba',
                'attr_start_snmp',
                'attr_start_sshd',
                'attr_start_syslog',
                'attr_start_x',
                'attr_start_xdmcp',
                'attr_tex_enable',
                'attr_timezone',
                'attr_tvout',
                'attr_vmware',
            ],
            [
                'id:pk',
                'export_id:fk',
                'name:s.64',
                'label:s.64',
                'kernel:s.128',
                'kernel_params:s.512',
                'hidden:b',
                'comment:s.1024',
            ]
        );
        #
        # ... client attributes ...
        $metaDB->schemaAddTable(
            'client_attr',
            [
                'id:pk',
                'client_id:fk',
                'name:s.128',
                'value:s.255',
            ]
        );
        foreach my $client ($metaDB->fetchClientByFilter()) {
            my %attrs;
            foreach my $key (keys %$client) {
                next if substr($key, 0, 5) ne 'attr_';
                my $attrValue = $client->{$key} || '';
                next if !length($attrValue);
                my $newAttrName = substr($key, 5);
                $attrs{$newAttrName} = $attrValue;
            }
            $metaDB->setClientAttrs($client->{id}, \%attrs);
        }
        $metaDB->schemaDropColumns(
            'client',
            [
                'attr_automnt_dir',
                'attr_automnt_src',
                'attr_country',
                'attr_dm_allow_shutdown',
                'attr_hw_graphic',
                'attr_hw_local_disk',
                'attr_hw_monitor',
                'attr_hw_mouse',
                'attr_late_dm',
                'attr_netbios_workgroup',
                'attr_nis_domain',
                'attr_nis_servers',
                'attr_sane_scanner',
                'attr_scratch',
                'attr_slxgrp',
                'attr_start_alsasound',
                'attr_start_atd',
                'attr_start_cron',
                'attr_start_dreshal',
                'attr_start_ntp',
                'attr_start_nfsv4',
                'attr_start_printer',
                'attr_start_samba',
                'attr_start_snmp',
                'attr_start_sshd',
                'attr_start_syslog',
                'attr_start_x',
                'attr_start_xdmcp',
                'attr_tex_enable',
                'attr_timezone',
                'attr_tvout',
                'attr_vmware',
            ],
            [
                'id:pk',
                'name:s.128',
                'mac:s.20',
                'boot_type:s.20',
                'unbootable:b',
                'kernel_params:s.128',
                'comment:s.1024',
            ]
        );
        #
        # ... group attributes ...
        $metaDB->schemaAddTable(
            'group_attr',
            [
                'id:pk',
                'group_id:fk',
                'name:s.128',
                'value:s.255',
            ]
        );
        foreach my $group ($metaDB->fetchGroupByFilter()) {
            my %attrs;
            foreach my $key (keys %$group) {
                next if substr($key, 0, 5) ne 'attr_';
                my $attrValue = $group->{$key} || '';
                next if !length($attrValue);
                my $newAttrName = substr($key, 5);
                $attrs{$newAttrName} = $attrValue;
            }
            $metaDB->setGroupAttrs($group->{id}, \%attrs);
        }
        $metaDB->schemaDropColumns(
            'groups',
            [
                'attr_automnt_dir',
                'attr_automnt_src',
                'attr_country',
                'attr_dm_allow_shutdown',
                'attr_hw_graphic',
                'attr_hw_local_disk',
                'attr_hw_monitor',
                'attr_hw_mouse',
                'attr_late_dm',
                'attr_netbios_workgroup',
                'attr_nis_domain',
                'attr_nis_servers',
                'attr_sane_scanner',
                'attr_scratch',
                'attr_slxgrp',
                'attr_start_alsasound',
                'attr_start_atd',
                'attr_start_cron',
                'attr_start_dreshal',
                'attr_start_ntp',
                'attr_start_nfsv4',
                'attr_start_printer',
                'attr_start_samba',
                'attr_start_snmp',
                'attr_start_sshd',
                'attr_start_syslog',
                'attr_start_x',
                'attr_start_xdmcp',
                'attr_tex_enable',
                'attr_timezone',
                'attr_tvout',
                'attr_vmware',
            ],
            [
                'id:pk',
                'name:s.128',
                'priority:i',
                'comment:s.1024',
            ]
        );
    
        return 1;
    },
    0.21 => sub {
        my $metaDB = shift;
    
        # add new table installed_plugin
        $metaDB->schemaAddTable(
            'installed_plugin', 
            [
                'id:pk',
                'vendor_os_id:fk',
                'plugin_name:s.64',
            ]
        );
    
        return 1;
    },
    0.22 => sub {
        my $metaDB = shift;
    
        # dummy schema change, just to trigger the attribute synchronization
        # into the default system
    
        return 1;
    },
    0.23 => sub {
        my $metaDB = shift;

        # add new column system.description
        $metaDB->schemaAddColumns(
            'system',
            [
                'description:s.512',
            ],
            undef,
            [
                'id:pk',
                'export_id:fk',
                'name:s.64',
                'label:s.64',
                'kernel:s.128',
                'kernel_params:s.512',
                'hidden:b',
                'description:s.512',
                'comment:s.1024',
            ]
        );
    
        return 1;
    },
    0.24 => sub {
        my $metaDB = shift;
    
        # split theme::name into theme::splash, theme::displaymanager and
        # theme::desktop
        foreach my $system ($metaDB->fetchSystemByFilter()) {
            my $attrs = $system->{attrs} || {};
            next if !exists $attrs->{'theme::name'};
            $attrs->{'theme::splash'} 
                = $attrs->{'theme::displaymanager'}
                = $attrs->{'theme::desktop'} 
                = $attrs->{'theme::name'};
            delete $attrs->{'theme::name'};
            $metaDB->setSystemAttrs($system->{id}, $attrs);
        }
    
        # force all plugin names to lowercase
        foreach my $vendorOS ($metaDB->fetchVendorOSByFilter()) {
            my @installedPlugins 
                = $metaDB->fetchInstalledPlugins($vendorOS->{id});
            foreach my $plugin (@installedPlugins) {
                my $pluginName = $plugin->{plugin_name};
                $metaDB->removeInstalledPlugin($vendorOS->{id}, $pluginName);
                $metaDB->addInstalledPlugin($vendorOS->{id}, lc($pluginName));
            }
        }

        return 1;
    },
    0.25 => sub {
        my $metaDB = shift;
    
        # drop attribute ramfs_screen
        $metaDB->removeAttributeByName('ramfs_screen');
    
        return 1;
    },
    0.26 => sub {
        my $metaDB = shift;
    
        # rename all exports and systems that contain a single colon to
        # the current naming scheme with a double colon
        foreach my $system ($metaDB->fetchSystemByFilter()) {
            if ($system->{name} =~ m{^([^:]+):([^:]+)$}) {
                if ($system->{label} eq $system->{name}) {
                    $system->{label} = "${1}::${2}";
                }
                $system->{name} = "${1}::${2}";
                $metaDB->changeSystem([ $system->{id} ], [ $system ]);
            }
        }
        foreach my $export ($metaDB->fetchExportByFilter()) {
            if ($export->{name} =~ m{^([^:]+):([^:]+)$}) {
                $export->{name} = "${1}::${2}";
                $metaDB->changeExport([ $export->{id} ], [ $export ]);
            }
        }
    
        return 1;
    },
    0.27 => sub {
        my $metaDB = shift;
    
        # add default vendor-OS, which holds info about the plugins that shall
        # be automatically installed into all vendor-OS that are being created.
        $metaDB->addVendorOS([{
            id      => '0',
            name    => '<<<default>>>',
            comment => 'holds default plugins for all vendor-OS',
        }]);
    
        return 1;
    },
    0.28 => sub {
        my $metaDB = shift;
    
        # correct effects of implementation error last time around that caused 
        # the default vendor-OS to not have any plugins at all - so we add
        # the default plugins here:
# OLTA: deactivated for good since this does not work anymore with newer
#       implementations (as addInstalledPlugin requires the table 
#       'installed_plugin_attr', which is going to be created in db-schema 
#       version 0.29 (see below)
#        $metaDB->addInstalledPlugin(0, 'theme');
    
        return 1;
    },
    0.29 => sub {
        my $metaDB = shift;
    
        # add new table installed_plugin_attrs
        $metaDB->schemaAddTable(
            'installed_plugin_attr', 
            [
                'id:pk',
                'installed_plugin_id:fk',
                'name:s.128',
                'value:s.255',
            ],
        );
    
        return 1;
    },
    0.30 => sub {
        my $metaDB = shift;
    
        # dummy schema change, just to trigger the attribute synchronization
        # into the default system (required since plugins have been added
        # and removed)
    
        return 1;
    },
    0.31 => sub {
        my $metaDB = shift;
    
        # dummy schema change, just to trigger the attribute synchronization
        # again, as the respective code has been extended
    
        return 1;
    },
    0.32 => sub {
        my $metaDB = shift;
    
        # dummy schema change, just to trigger the attribute synchronization,
        # as the 'theme' plugin has been removed
    
        return 1;
    },
    0.33 => sub {
        my $metaDB = shift;

        # add new column meta.plugin_info_hash
        $metaDB->schemaAddColumns(
            'meta',
            [
                'plugin_info_hash:s.32',
            ],
            undef,
            [
                'plugin_info_hash:s.32',
                'schema_version:s.5',
            ]
        );
    
        return 1;
    },
    0.34 => sub {
        my $metaDB = shift;

        # turn client fields 'boot_type', 'kernel_params' and 'unbootable' 
        # into attributes:
        foreach my $client ($metaDB->fetchClientByFilter()) {
            my $attrs = $metaDB->fetchClientAttrs($client->{id});
            $attrs->{boot_type}            = $client->{boot_type} || 'pxe';
            $attrs->{kernel_params_client} = $client->{kernel_params};
            $attrs->{unbootable}           = $client->{unbootable};
            $metaDB->setClientAttrs($client->{id}, $attrs);
        }
        $metaDB->schemaDropColumns(
            'client',
            [
                'boot_type',
                'kernel_params',
                'unbootable',
            ],
            [
                'id:pk',
                'name:s.128',
                'mac:s.20',
                'comment:s.1024',
            ]
        );
    
        # turn system fields 'hidden' and 'kernel_params' into attributes:
        foreach my $system ($metaDB->fetchSystemByFilter()) {
            my $attrs = $metaDB->fetchSystemAttrs($system->{id});
            $attrs->{hidden}        = $system->{hidden};
            $attrs->{kernel_params} = $system->{kernel_params};
            $metaDB->setSystemAttrs($system->{id}, $attrs);
        }
        $metaDB->schemaDropColumns(
            'system',
            [
                'hidden',
                'kernel_params',
            ],
            [
                'id:pk',
                'export_id:fk',
                'name:s.64',
                'label:s.64',
                'kernel:s.128',
                'description:s.512',
                'comment:s.1024',
            ]
        );
    
        return 1;
    },
    0.35 => sub {
        my $metaDB = shift;

        # add new column system.pxe_prefix_ip
        $metaDB->schemaAddColumns(
            'system',
            [
                'pxe_prefix_ip:s.16',
            ],
            undef
        );
    
        return 1;
    },
    0.36 => sub {
        my $metaDB = shift;

        # value 'preboot-cd' in client-attr 'boot_type' has been changed
        # to 'preboot', and a separate attribute 'preboot_media' has been
        # introduced:
        foreach my $client ($metaDB->fetchClientByFilter()) {
            my $attrs = $metaDB->fetchClientAttrs($client->{id});
            if ($attrs->{boot_type} eq 'preboot-cd') {
                $attrs->{boot_type} = 'preboot';
                $attrs->{preboot_media} = 'cd';
                $metaDB->setClientAttrs($client->{id}, $attrs);
            }
        }
    
        return 1;
    },
);

1;