first release
continuous-integration/drone/tag Build is passing Details

This commit is contained in:
Paolo Asperti 2019-05-05 23:42:20 +02:00
parent e685ce4a43
commit 83bf64bd39
Signed by: paspo
GPG Key ID: 06D46905D19D5182
49 changed files with 1495 additions and 0 deletions

83
.drone.yml Normal file
View File

@ -0,0 +1,83 @@
kind: pipeline
name: default
steps:
- name: create_targz
image: registry.libreschool.org/libreschool-automation/rpm-pack:latest
pull: always
settings:
files:
- root
- createlinks.pl
- README.md
- LICENSE
when:
event: [ push, tag ]
- name: build
image: registry.libreschool.org/libreschool-automation/rpm-build:7
pull: always
commands:
- yum-builddep -y libreschool-client-manager.spec
- rpmbuild -ba libreschool-client-manager.spec
- mkdir out ; mv *.rpm noarch/*.rpm out/
when:
event: [ push, tag ]
- name: sign
image: registry.libreschool.org/libreschool-automation/rpm-sign:latest
pull: always
settings:
files: out/*.rpm
gpg_key:
from_secret: gpg_key
passphrase:
from_secret: passphrase
when:
event: tag
- name: gitea_release
image: plugins/gitea-release
settings:
files: out/*.rpm
checksum:
- md5
- sha1
- sha256
- sha512
api_key:
from_secret: gitea_token
base_url: https://git.libreschool.org
file_exists: skip
when:
event: tag
- name: repo_upload
image: appleboy/drone-scp
settings:
source: out/*.noarch.rpm
target: /vhosts/repo.libreschool.org/https/nethserver/7/devel/x86_64/Packages/
host: repo.libreschool.org
strip_components: 1
username:
from_secret: ssh_username
key:
from_secret: ssh_key
when:
event: tag
repo: LibreSchool/libreschool-client-manager
- name: repo_upload_srpms
image: appleboy/drone-scp
settings:
source: out/*.src.rpm
target: /vhosts/repo.libreschool.org/https/nethserver/7/devel/Source/SPackages/
host: repo.libreschool.org
strip_components: 1
username:
from_secret: ssh_username
key:
from_secret: ssh_key
when:
event: tag
repo: LibreSchool/libreschool-client-manager

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[Makefile]
indent_style = tab
[**.php]
indent_style = space
indent_size = 4
[**.{js,coffee,html,less,css,json}]
indent_style = space
indent_size = 2
[**.md]
trim_trailing_whitespace = false

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.sh
*.tar.*
*.rpm
*.log

View File

@ -1,2 +1,41 @@
# libreschool-client-manager
This software uses Ansible Playbooks to configure all connected clients.
## Available Events
Adopt a machine (which should already exist in the *hosts* db):
# signal-event client-adopt pc-99 username password
Reboot a machine:
# signal-event client-reboot pc-01
Reboot all machines:
# signal-event client-reboot-all
Force reconfiguration of a machine:
# signal-event client-reconfigure pc-02
Force reconfiguration of all machines:
# signal-event client-reconfigure-all
Shutdown a machine:
# signal-event client-shutdown pc-02
Shutdown all machines:
# signal-event client-shutdown-all
Update source configuration timestamp:
# signal-event client-update-lastconfig
Wakeup a machine:
# signal-event client-wake pc-02
Wakeup all machines:
# signal-event client-wake-all
### Support for a group of machines
If you defined a group of machines, you can send signals to groups also.
# db hosts set epoptes_lab1 group members 'pc-01 pc-02 pc-03' master pc-01
# signal-event client-wake epoptes_lab1
## E-smith DB reference
The general configuration is stored in the *client-manager* key. Properties of such key are:
* *lastConfig*: timestamp of the actual configuration set
* *ManagementKeyFile*: management user's SSH public key
* *ManagementPrivateKeyFile*: management user's SSH private key
All host-specific information are stored in the *hosts* db. For each host we have:
* *sshUser*: ssh user used to connect to the host. This is set when SSH adoption is successful

16
createlinks.pl Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/perl -w
use esmith::Build::CreateLinks qw(:all);
use File::Basename;
use File::Path;
event_actions('client-adopt', 'client-adopt' => 50);
event_actions('client-reboot-all', 'client-reboot-all' => 50);
event_actions('client-reboot', 'client-reboot' => 50);
event_actions('client-reconfigure-all', 'client-reconfigure-all' => 50);
event_actions('client-reconfigure', 'client-reconfigure' => 50);
event_actions('client-shutdown-all', 'client-shutdown-all' => 50);
event_actions('client-shutdown', 'client-shutdown' => 50);
event_actions('client-update-lastconfig', 'client-update-lastconfig' => 50);
event_actions('client-wake-all', 'client-wake-all' => 50);
event_actions('client-wake', 'client-wake' => 50);

View File

@ -0,0 +1,91 @@
Summary: LibreSchool Client Manager
Name: libreschool-client-manager
Version: 1.0
Release: 1.ns7
License: GPL
BuildArch: noarch
BuildRoot: %{_tmppath}/%{name}-root
Source0: %{name}-%{version}.tar.gz
Source1: createlinks.pl
Source2: LICENSE
URL: %{url_prefix}/%{name}
Group: System Environment/Daemons
Requires: libreschool-release
Requires: ansible
Requires: httpd, php, sudo, php-xml, php-mcrypt
Requires: nethserver-base
Requires: ansible
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
Requires(pre): /usr/sbin/useradd, /usr/bin/getent
Requires(postun): /usr/sbin/userdel
BuildRequires: nethserver-devtools > 1.0.1
BuildRequires: systemd
%description
LibreSchool Ansible base structure for client management
%prep
%setup
%build
%{makedocs}
perl createlinks.pl
%install
rm -rf $RPM_BUILD_ROOT
(cd root ; find . -depth -not -name '*.orig' -print | cpio -dump $RPM_BUILD_ROOT)
%{genfilelist} $RPM_BUILD_ROOT > %{name}-%{version}-%{release}-filelist
echo "%doc LICENSE" >> %{name}-%{version}-%{release}-filelist
mkdir -p %{buildroot}%{_localstatedir}/log/client-manager
mkdir -p %{buildroot}%{_localstatedir}/cache/client-manager
%files -f %{name}-%{version}-%{release}-filelist
%attr(0755,srvmgr,srvmgr) %dir %{_localstatedir}/cache/client-manager
%attr(0755,srvmgr,srvmgr) %dir %{_localstatedir}/log/client-manager
%defattr(-,root,root)
%pre
/usr/bin/getent passwd amgmt || /usr/sbin/useradd -r -g wheel -d /home/amgmt -s /sbin/nologin amgmt
if [ "$1" = 2 ]; then
#upgrade
/usr/bin/systemctl stop client-manager >/dev/null 2>&1 || :
fi
%post
/etc/e-smith/events/actions/initialize-default-databases
[ -e /usr/sbin/client-manager ] && [ ! -L /usr/sbin/client-manager ] && rm /usr/sbin/client-manager
[ ! -L /usr/sbin/client-manager ] && ln -s httpd /usr/sbin/client-manager
/sbin/e-smith/expand-template /etc/sudoers
/sbin/e-smith/expand-template /etc/httpd/client-manager/httpd.conf
/sbin/e-smith/expand-template /etc/ansible/reconfigure_clients.yml
/sbin/e-smith/signal-event runlevel-adjust
/usr/bin/systemctl --no-block start client-manager >/dev/null 2>&1 || :
/sbin/e-smith/signal-event firewall-adjust
%preun
#if [ "$1" = 1 ]; then
# #upgrade
#fi
if [ "$1" = 0 ]; then
#uninstallation
/usr/bin/systemctl stop client-manager >/dev/null 2>&1 || :
fi
%postun
#if [ "$1" = 1 ]; then
# #upgrade
#fi
if [ "$1" = 0 ]; then
#uninstallation
/usr/sbin/userdel amgmt
/sbin/e-smith/db configuration delete client-manager
/sbin/e-smith/expand-template /etc/sudoers
/sbin/e-smith/signal-event runlevel-adjust
/sbin/e-smith/signal-event firewall-adjust
fi
%changelog
* Sun Apr 28 2019 Paolo Asperti <paolo@asperti.com> - 1.0-1.ns7
- First release for NethServer 7

View File

@ -0,0 +1,13 @@
[defaults]
host_key_checking = False
inventory = /etc/ansible/inventory/
remote_user = amgmt
private_key_file=/home/amgmt/.ssh/id_rsa
nocows = 1
log_path = /var/log/ansible.log
[privilege_escalation]
become=True
become_method=sudo
become_user=root
become_ask_pass=False

View File

@ -0,0 +1,78 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
use JSON;
my $db_hosts = esmith::DB::db->open('hosts')
|| die( "Could not open e-smith db (" . esmith::DB::db->error . ")\n" );
my $db_configuration = esmith::DB::db->open('configuration')
|| die( "Could not open e-smith db (" . esmith::DB::db->error . ")\n" );
my @hostnames = ();
my %vars = ();
my %hostvars = ();
my $default_user = "amgmt";
my $ansible_ssh_private_key_file =
$db_configuration->get_prop( 'client-manager', 'ManagementPrivateKeyFile' )
|| '/home/amgmt/.ssh/id_rsa';
$vars{'configuration_timestamp'} =
qx/find \/etc\/ansible\/roles -type f -printf '%T@\n' | sort -r | head -n 1/;
chomp( $vars{'configuration_timestamp'} );
$vars{"ansible_ssh_private_key_file"} =
$db_configuration->get_prop( 'client-manager', 'ManagementPrivateKeyFile' );
my $domainname = $db_configuration->get_prop( 'DomainName', 'type' );
my @items = $db_hosts->get_all_by_prop('type' => 'local');
foreach my $item (@items) {
push @hostnames, $item->key . '.' . $domainname;
my $ansible_user = $item->prop('sshUser') || $default_user;
my %vars = (
'ansible_ssh_private_key_file' => $ansible_ssh_private_key_file,
'ansible_user' => $ansible_user
);
$hostvars{ $item->key . '.' . $domainname } = {%vars};
}
my %output = (
'all' => {
'hosts' => [@hostnames],
'vars' => {%vars},
'children' => ['clients','ungrouped'],
},
'_meta' => {
'hostvars' => {%hostvars}
},
'ungrouped' => [],
'clients' => [@hostnames]
);
my @groups = $db_hosts->get_all_by_prop('type' => 'group');
foreach my $group (@groups) {
my $members_str = $group->prop('members') || '';
my @members = split ' ', $members_str;
for my $member (@members) {
$member = $member . '.' . $domainname;
}
my %props = $group->props;
delete( $props{'members'} );
delete( $props{'type'} );
$output{ $group->key } = { 'hosts' => [@members], 'vars' => {%props} };
push $output{'all'}{'children'}, $group->key;
}
my $json = encode_json \%output;
print "$json\n";
exit 0;

View File

@ -0,0 +1,38 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
use JSON;
my %ENV;
my @d = `bash -c 'source $ARGV[0] && echo db=\${db:-configuration}'`;
for my $item (@d) {
chomp $item;
print "ITEM: $item \n";
my ( $k, $v ) = split /=/, $item, 2;
$v =~ s/^(['"])(.*)\1/$2/; #' fix highlighter
$v =~ s/\$([a-zA-Z]\w*)/$ENV{$1}/g;
$v =~ s/`(.*?)`/`$1`/ge; #dangerous
$ENV{$k} = $v;
}
my $db = esmith::DB::db->open( $ENV{'db'} ) || die("{}");
my @items = $db->get_all();
my %out;
foreach my $item (@items) {
my %props = $item->props();
if ( scalar( keys %props ) <= 1 ) {
$out{ $item->key } = $props{'type'};
}
else {
$out{ $item->key } = { $item->props() };
}
}
print encode_json \%out;

View File

@ -0,0 +1,21 @@
---
- name: Import e-smith configuration db
e-smith:
register: esmith
delegate_to: localhost
- name: Add LibreSchool signing key
apt_key:
url: http://repo.libreschool.org/libreschool.asc
state: present
- name: Add LibreSchool repository
apt_repository:
repo: deb http://repo.libreschool.org/ubuntu {{ ansible_distribution_release }} main
state: present
- name: Update repositories cache and install "libreschool-client" package
apt:
name: libreschool-client
update_cache: yes

View File

@ -0,0 +1,4 @@
---
- name: rerun-libreschool-client
shell: "(/usr/sbin/libreschool-client & )"
ignore_errors: yes

View File

@ -0,0 +1,19 @@
---
- name: Check if libreschool-client is installed
shell: dpkg-query -W -f='${Status}' libreschool-client | grep 'install ok installed'
register: lsclient_installed
failed_when: no
changed_when: no
- name: Updating libreschool-client
ini_file:
path: /etc/libreschool-client/libreschool-client.conf
option: last_config
section: server
no_extra_spaces: yes
value: "{{ configuration_timestamp }}"
notify: rerun-libreschool-client
when: lsclient_installed.rc == 0
# TODO: ini_file always return "changed" even if it's not

View File

@ -0,0 +1,4 @@
# we also update via cron because you can change ansible playbooks
# via other methods than the web ui
*/5 * * * * root /sbin/e-smith/signal-event client-update-lastconfig

View File

@ -0,0 +1 @@
/home/amgmt/.ssh/id_rsa.pub

View File

@ -0,0 +1 @@
/home/amgmt/.ssh/id_rsa

View File

@ -0,0 +1 @@
3000

View File

@ -0,0 +1 @@
green

View File

@ -0,0 +1 @@
enabled

View File

@ -0,0 +1 @@
service

View File

@ -0,0 +1,47 @@
#!/bin/bash
EVENT="$1"
HOST="$2"
USER="$3"
PASS="$4"
HOST_IN_DB=$(db hosts show "$HOST" >/dev/null && echo yes)
if [[ "$HOST_IN_DB" = "" ]] ; then
echo "Host '$HOST' not in DB (or not registered)"
exit 1
fi
ping -c1 "$HOST" >/dev/null || (
echo "Can't ping $HOST"
exit 1
)
sshpass -p "${PASS}" ssh -t -o StrictHostKeyChecking=no "${USER}@${HOST}" "whoami" || (
echo "Can't login to $HOST as $USER"
exit 1
)
SUDO=$(sshpass -p "${PASS}" ssh -t -o StrictHostKeyChecking=no "${USER}@${HOST}" "which sudo")
if [[ "$SUDO" = "" ]] ; then
echo "Sudo not present in $HOST"
exit 1
fi
AMIROOT=$(sshpass -p "${PASS}" ssh -t -o StrictHostKeyChecking=no "${USER}@${HOST}" "echo '${PASS}' | sudo -S whoami")
if [[ "$AMIROOT" = "" ]]; then
echo "Not enough privileges for $HOST"
exit 1
fi
PUB=$(db configuration getprop client-manager ManagementKeyFile)
cat $PUB | \
sshpass -p "${PASS}" ssh -t -o StrictHostKeyChecking=no "${USER}@${HOST}" "echo '${PASS}' | sudo -S mkdir /root/.ssh >/dev/null ; sudo tee -a /root/.ssh/authorized_keys"
PRIV=$(db configuration getprop client-manager ManagementPrivateKeyFile)
ssh -t -o StrictHostKeyChecking=no -i $PRIV "root@${HOST}" "whoami" || (
echo "Something went wrong"
exit 1
)
db hosts setprop ${HOST} sshUser root

View File

@ -0,0 +1,35 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my $event = $ARGV[0];
defined $ARGV[1] || die "No host specified";
my $host = $ARGV[1];
reboot_host($host);
exit 0;
sub reboot_host
{
my ($hostName) = @_;
#------------------------------------------------------------
# Reboot the host
#------------------------------------------------------------
my $h = $db_hosts->get($hostName) or die "No host record for $hostName";
my $user = $h->prop('sshUser') || 'amgmt';
system("/usr/bin/sudo -u amgmt /usr/bin/ssh -o StrictHostKeyChecking=no $user\@$hostName /usr/bin/sudo /sbin/reboot") == 0
or die "Error rebooting $hostName";
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my @items = $db_hosts->get_all('local');
foreach my $item (@items) {
my $hostName = $item->key;
my $user = $item->prop('sshUser') || 'amgmt';
system("/usr/bin/sudo -u amgmt /usr/bin/ssh -o StrictHostKeyChecking=no $user\@$hostName /usr/bin/sudo /sbin/reboot &") == 0
or die "Error rebooting $hostName";
}
exit 0;

View File

@ -0,0 +1,37 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my $db_configuration = esmith::DB::db->open('configuration') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my $domainname = $db_configuration->get_prop('DomainName','type');
my $event = $ARGV [0];
defined $ARGV[1] || die "No host specied";
my $host = $ARGV[1];
reconfigure_host($host);
exit 0;
sub reconfigure_host
{
my ($hostName) = @_;
my $h = $db_hosts->get($hostName) or die "No host record for user $hostName";
my $fqdn = $hostName . '.' . $domainname;
# ANSIBLE_CONFIG=/etc/ansible/ansible-libreschool.cfg ansible-playbook --list-hosts -v /etc/ansible/reconfigure_clients.yml
system("ANSIBLE_CONFIG=/etc/ansible/ansible-libreschool.cfg /usr/bin/ansible-playbook -l $fqdn -v /etc/ansible/reconfigure_clients.yml") == 0
or die "Error reconfiguring $hostName";
}

View File

@ -0,0 +1,5 @@
#!/bin/bash
#/usr/bin/sudo -u amgmt
export ANSIBLE_CONFIG=/etc/ansible/ansible-libreschool.cfg
/usr/bin/ansible-playbook -v /etc/ansible/reconfigure_clients.yml

View File

@ -0,0 +1,35 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my $event = $ARGV[0];
defined $ARGV[1] || die "No host specified";
my $host = $ARGV[1];
shutdown_host($host);
exit 0;
sub shutdown_host
{
my ($hostName) = @_;
#------------------------------------------------------------
# Shutdown the host
#------------------------------------------------------------
my $h = $db_hosts->get($hostName) or die "No host record for $hostName";
my $user = $h->prop('sshUser') || 'amgmt';
system("/usr/bin/sudo -u amgmt /usr/bin/ssh -o StrictHostKeyChecking=no $user\@$hostName /usr/bin/sudo /sbin/poweroff") == 0
or die "Error shutting down $hostName";
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my @items = $db_hosts->get_all('local');
foreach my $item (@items) {
my $hostName = $item->key;
my $user = $item->prop('sshUser') || 'amgmt';
system("/usr/bin/sudo -u amgmt /usr/bin/ssh -o StrictHostKeyChecking=no $user\@$hostName /usr/bin/sudo /sbin/poweroff &") == 0
or die "Error shutting down $hostName";
}
exit 0;

View File

@ -0,0 +1,5 @@
#!/bin/sh
LAST_CONFIG=$(/bin/find /etc/ansible/roles -type f -printf '%T@\n' | /bin/sort -r | /usr/bin/head -n 1)
/sbin/e-smith/db configuration setprop client-manager lastConfig "${LAST_CONFIG}"

View File

@ -0,0 +1,35 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my $event = $ARGV[0];
defined $ARGV[1] || die "No host specified";
my $host = $ARGV[1];
wake_host($host);
exit 0;
sub wake_host
{
my ($hostName) = @_;
#------------------------------------------------------------
# Wake up the host
#------------------------------------------------------------
my $h = $db_hosts->get($hostName) or die "No host record for $hostName";
my $client = $h[0];
my $mac = $client->prop('MacAddress');
system("/sbin/ether-wake -b -i lan0 $mac") == 0
or die "Error waking up $hostName";
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/perl -w
package esmith;
use strict;
use Errno;
use esmith::DB::db;
use IO::File;
use English;
my $db_hosts = esmith::DB::db->open('hosts') || die("Could not open e-smith db (" . esmith::DB::db->error . ")\n");
my @items = $db_hosts->get_all('local');
foreach my $item (@items) {
my $hostName = $item->key;
my $mac = $item->prop('MacAddress');
system("/sbin/ether-wake -b -i lan0 $mac &") == 0
or die "Error waking up $hostName";
}
exit 0;

View File

@ -0,0 +1,7 @@
---
- hosts: all
user: root
roles:
- common

View File

@ -0,0 +1 @@
- update-libreschool-client

View File

@ -0,0 +1,10 @@
{
#
# 00template_defaults
#
$localAccess = '';
$port = ${'client-manager'}{TCPPort} || "3000";
"";
}

View File

@ -0,0 +1,46 @@
{
$OUT .= <<HERE;
Listen $port
HostnameLookups off
ServerAdmin root@$DomainName
ServerRoot /etc/httpd
ServerTokens ProductOnly
User srvmgr
Group srvmgr
Include conf.modules.d/*.conf
ErrorLog /var/log/client-manager/error_log
LogLevel warn
HERE
$OUT .= <<HERE;
PidFile /var/run/client-manager.pid
ScoreBoardFile /var/run/client-manager.scoreboard
UseCanonicalName off
LogFormat "%h %l %u %t \\"%r\\" %>s %b" common
LogFormat "%{User-agent}i" agent
CustomLog /var/log/client-manager/access_log common
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 15
MaxClients 150
MaxRequestsPerChild 100
MinSpareServers 1
MaxSpareServers 5
StartServers 1
Timeout 300
DirectoryIndex index.php
AccessFileName .htaccess
HERE
}

View File

@ -0,0 +1,7 @@
<Directory />
Options None
AllowOverride None
order deny,allow
deny from all
allow from none
</Directory>

View File

@ -0,0 +1,16 @@
DocumentRoot /usr/share/client-manager
<Directory /usr/share/client-manager/>
Options +Indexes
Order Deny,Allow
Deny from all
Allow from { $localAccess || 'all' }
AddType application/x-httpd-php .php
Options +FollowSymlinks
RewriteEngine on
RewriteBase /
RewriteRule ^ping$ index.php/ping [L]
php_value session.save_path /var/cache/client-manager
php_flag magic_quotes_gpc off
</Directory>

View File

@ -0,0 +1,4 @@
AddDefaultCharset UTF-8
<IfModule mime_module>
TypesConfig /etc/mime.types
</IfModule>

View File

@ -0,0 +1 @@
Include conf.d/php.conf

View File

@ -0,0 +1,11 @@
{
$OUT = 'srvmgr ALL=NOPASSWD: /bin/cat ';
use esmith::ConfigDB;
my $confdb = esmith::ConfigDB->open;
my $backup = $confdb->get('client-manager') or die "No client-manager db entry found\n";
my $ManagementKeyFile = $backup->prop('ManagementKeyFile') || '/home/amgmt/.ssh/id_rsa.pub';
$OUT = 'srvmgr ALL=NOPASSWD: /bin/cat ' . $ManagementKeyFile;
}

View File

@ -0,0 +1,9 @@
/var/log/client-manager/*log {
missingok
notifempty
sharedscripts
delaycompress
postrotate
/bin/systemctl reload client-manager > /dev/null 2>/dev/null || true
endscript
}

1
root/etc/sudoers.d/amgmt Normal file
View File

@ -0,0 +1 @@
amgmt ALL=(ALL:ALL) NOPASSWD:ALL

View File

@ -0,0 +1,7 @@
#
# This file can be used to set additional environment variables for
# the client-manager process, or pass additional options to the
# client-manager executable. See client-manager.service unit.
#
LANG=C

View File

@ -0,0 +1,7 @@
[Unit]
Description=Graceful restart of client-manager
Requisite=client-manager.service
[Service]
Type=oneshot
ExecStart=/usr/sbin/httpd -f /etc/httpd/client-manager/httpd.conf -k graceful

View File

@ -0,0 +1,6 @@
[Unit]
Description=Delayed graceful restart of client-manager
Conflicts=client-manager-reload.service
[Timer]
OnActiveSec=5

View File

@ -0,0 +1,14 @@
[Unit]
Description=LibreSchool Client Manager httpd instance
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=simple
EnvironmentFile=/etc/sysconfig/client-manager
ExecStart=/usr/sbin/httpd -f /etc/httpd/client-manager/httpd.conf -c 'MaxConnectionsPerChild 12' -DFOREGROUND
ExecReload=/usr/sbin/httpd -f /etc/httpd/client-manager/httpd.conf -k graceful
LimitNOFILE=4096
KillMode=process
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,391 @@
<?php
/*
* Copyright (C) 2011 Nethesis S.r.l.
*
* This script is part of NethServer.
*
* NethServer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NethServer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NethServer. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Read and write parameters into SME DB
*
* Ths class implements an interface to SME database executing the command /sbin/e-smith/db with sudo.
* The class needs /etc/sudoers configurazione. In the sudoers file you must have something like this:
* <code>
* Cmnd_Alias SME = /sbin/e-smith/db, /sbin/e-smith/signal-event
* www ALL=NOPASSWD: SME
* </code>
*
*
* @author Giacomo Sanchietti <giacomo.sanchietti@nethesis.it>
* @author Davide Principi <davide.principi@nethesis.it>
* @since 1.0
* @internal
*/
class EsmithDatabase
{
/**
* @var SME DB database command
* */
private $command = "/usr/bin/sudo /sbin/e-smith/db";
/**
* @var $db Database name, it's translated into the db file path. For example: /home/e-smith/db/testdb
* */
private $db;
/**
* This static class variable shares a socket connection to smwingsd
*
* @var resource
*/
private static $socket;
/**
* Construct an object to access a SME Configuration database file
* with $user's privileges.
*
* @param string $database Database name
*/
public function __construct($database)
{
if ( ! $database)
sprintf("%s: You must provide a valid database name.", get_class($this));
$this->db = $database;
}
public function getAll($type = NULL)
{
$output = "";
$ret = $this->dbRead('getjson', array(), $output);
if ($ret !== 0) {
sprintf("%s: internal database command failed!", __CLASS__);
}
$data = json_decode($output, TRUE);
if ( ! is_array($data)) {
sprintf("%s: unexpected json string `%s`", __CLASS__, substr($output, 0, 8));
}
$result = array();
foreach ($data as $item) {
// Apply type check filter:
if (isset($type) && $type !== $item['type']) {
continue;
}
$props = isset($item['props']) ? $item['props'] : array();
$result[$item['name']] = array_merge($props, array('type' => $item['type']));
}
return $result;
}
public function getAllByProp($propName = NULL, $propValue = NULL)
{
$output = "";
$ret = $this->dbRead('getjson', array(), $output);
if ($ret !== 0) {
sprintf("%s: internal database command failed!", __CLASS__);
}
$data = json_decode($output, TRUE);
if ( ! is_array($data)) {
sprintf("%s: unexpected json string `%s`", __CLASS__, substr($output, 0, 8));
}
$result = array();
foreach ($data as $item) {
$item['props']['type'] = $item['type'];
$item['props']['name'] = $item['name'];
if (isset($propName) && isset($propValue)) {
if (isset($item['props'][$propName]) && $propValue == $item['props'][$propName]) {
$result[$item['name']] = $item['props'];
}
} else {
$result[$item['name']] = $item['props'];
}
}
return $result;
}
public function getKey($key)
{
$output = '';
$ret = $this->dbRead('getjson', array($key), $output);
if ($ret !== 0) {
sprintf("%s: internal database command failed", __CLASS__);
}
$data = json_decode($output, TRUE);
if ($data === 1) {
// Key has not been found
return array();
} elseif ( ! is_array($data)) {
sprintf("%s: unexpected json string `%s`", __CLASS__, substr($output, 0, 8));
}
$data['props']['type'] = $data['type'];
$data['props']['name'] = $data['name'];
return $data['props'];
}
public function getKeyValue($key)
{
$output = '';
$ret = $this->dbRead('getjson', array($key), $output);
if ($ret !== 0) {
sprintf("%s: internal database command failed", __CLASS__);
}
$data = json_decode($output, TRUE);
if ($data === 1) {
// Key has not been found
return array();
} elseif ( ! is_array($data)) {
sprintf("%s: unexpected json string `%s`", __CLASS__, substr($output, 0, 8));
}
return $data['type'];
}
public function setKey($key, $type, $props)
{
$output = NULL;
$ret = $this->dbExec('set', $this->prepareArguments($key, $type, $props), $output);
return ($ret == 0);
}
public function deleteKey($key)
{
$output = NULL;
$output = NULL;
$ret = $this->dbExec('delete', $this->prepareArguments($key), $output);
return ($ret == 0);
}
/**
* Return the type of a key
* Act like: /sbin/e-smith/db dbfile gettype key
*
* @param string $key the key to retrieve
* @access public
* @return string the type of the key
*/
public function getType($key)
{
$output = NULL;
$ret = $this->dbRead('gettype', array($key), $output);
return trim($output);
}
/**
* Set the type of a key
* Act like: /sbin/e-smith/db dbfile settype key type
*
* @param string $key the key to change
* @param string $type the new type
* @access public
* @return bool true on success, FALSE otherwise
*/
public function setType($key, $type)
{
$output = NULL;
$ret = $this->dbExec('settype', $this->prepareArguments($key, $type), $output);
return ($ret == 0);
}
public function getProp($key, $prop)
{
$output = NULL;
$ret = $this->dbRead('getprop', array($key, $prop), $output);
return trim($output);
}
public function setProp($key, $props)
{
$output = NULL;
$ret = $this->dbExec('setprop', $this->prepareArguments($key, $props), $output);
return ($ret == 0);
}
public function delProp($key, $props)
{
$output = NULL;
$ret = $this->dbExec('delprop', array_merge(array($key), array_values($props)), $output);
return ($ret == 0);
}
/**
* Execute the db command
* @param string $command The command to invoke
* @param array $args The command arguments
* @param string &$output The output from the command process
* @return int The command exit code
*/
private function dbExec($command, $args, &$output)
{
// prepend the database name and command
array_unshift($args, $this->db, $command);
$exitCode = 0;
$oArr = array();
exec($this->command . ' ' . implode(' ', array_map('escapeshellarg', $args)), $oArr, $exitCode);
$output = implode("\n", $oArr);
return $exitCode;
}
/**
* Read db data from memory cache daemon
* @param string $command The command to invoke
* @param array $args The command arguments
* @param string &$output The output from the read socket
* @return int The command exit code
*/
private function dbRead($command, $args, &$output)
{
if ( ! isset(self::$socket) ) {
$socketPath = '/var/run/smwingsd.sock';
$errno = 0;
$errstr = '';
self::$socket = fsockopen('unix://' . $socketPath, -1, $errno, $errstr);
if( ! is_resource(self::$socket)) {
sprintf("Invalid socket (%d): %s. Fall back to exec().", $errno, $errstr);
}
}
if ( ! is_resource(self::$socket)) {
return $this->dbExec($command, call_user_func_array(array($this, 'prepareArguments'), $args), $output);
}
// prepend the database name and command
array_unshift($args, $this->db, $command);
$ret = $this->sendMessage(self::$socket, 0x10, $args);
if ( ! $ret ) {
return 1;
}
$ret = $this->recvMessage(self::$socket);
if ( ! $ret ) {
return 1;
}
if ($command === 'getjson') {
$output = $ret;
} else {
$output = json_decode($ret, TRUE);
}
return 0;
}
private function sendMessage($socket, $type, $args = array())
{
$payload = json_encode($args);
$data = pack('CN', (int) $type, strlen($payload)) . $payload;
$written = fwrite($socket, $data);
if ($written !== strlen($data)) {
echo 'Socket write error';
}
return TRUE;
}
private function recvMessage($socket)
{
$buf = $this->safeRead($socket, 5);
if ($buf === FALSE) {
echo 'Socket read error';
return false;
}
$header = unpack('Ctype/Nsize', $buf);
if ( ! is_array($header)) {
echo 'Invalid message header';
return false;
}
$message = $this->safeRead($socket, $header['size']);
if ($message === FALSE) {
echo 'Socket read error';
return false;
}
if ($header['type'] & 0x02) {
return NULL;
}
return $message;
}
private function safeRead($socket, $size)
{
$buffer = "";
$count = 0;
while($count < $size) {
if(feof($socket)) {
return FALSE;
}
$chunk = fread($socket, $size - $count);
$count += strlen($chunk);
if($chunk === FALSE) {
return FALSE;
}
$buffer .= $chunk;
}
return $buffer;
}
/**
* Take arbitrary arguments and flattenize to an array
*
* @param mixed $_
* @return array
*/
private function prepareArguments()
{
$args = array();
foreach (func_get_args() as $arg) {
if (is_array($arg)) {
foreach ($arg as $propName => $propValue) {
$args[] = $propName;
$args[] = $propValue;
}
} else {
$args[] = (String) $arg;
}
}
return $args;
}
}

View File

@ -0,0 +1,253 @@
<?php
include_once("EsmithDatabase.php");
define('CONFIG_KEY', 'client-manager');
$hosts_db = new EsmithDatabase('hosts');
$config_db = new EsmithDatabase('configuration');
$dhcp_db = new EsmithDatabase('dhcp');
$uri = $_SERVER['REQUEST_URI'];
// add leading '/' to uri string, works if $uri is empty
$uri = (substr($uri, 0, 1) == '/') ? $uri : '/' . $uri;
$pos = strpos($uri, '?');
if (false !== $pos) {
$route = substr($uri, 0, (int) $pos);
} else {
$route = $uri;
}
// remove trailing '/'
$route = rtrim($route, '/');
$query_string = $_SERVER['QUERY_STRING'];
$method = $_SERVER["REQUEST_METHOD"];
$get_params = array();
parse_str($query_string, $get_params);
// determine internal function name (all lowercase, dash2underscore, named ROUTE_methodname_routename )
$function = preg_replace(':\-:', '_', strtolower($route));
$function = preg_replace(':^/:', 'ROUTE_'.$method.'_', $function);
if (function_exists($function)) {
$function();
} else {
die('Go away.');
}
function getClientFromUUID($uuid=false) {
global $hosts_db;
$hosts = $hosts_db->getAllByProp('uuid',$uuid);
if (count($hosts)>0) {
return array_pop($hosts);
} else {
return false;
}
}
function getClientFromIP($ip=false) {
global $hosts_db;
$hosts = $hosts_db->getAllByProp('IpAddress',$ip);
if (count($hosts)>0) {
return array_pop($hosts);
} else {
return false;
}
}
function getClientMac($ip=false) {
if (false === $ip) {
$ip = $_SERVER['REMOTE_ADDR'];
}
$result = pingHost($ip,1);
if (!$result) return false;
$macAddr=false;
$arptable=`arp -n $ip`;
$lines=explode("\n", $arptable);
foreach($lines as $line) {
$cols=preg_split('/\s+/', trim($line));
if ($cols[0]==$ip)
{
return $cols[2];
}
}
return false;
}
function pingHost($host, $timeout=1) {
$pingresult = exec("/bin/ping -c 1 -t 2 -W $timeout $host", $output, $retvar);
return (0 == $retvar);
}
function filtered_json() {
global $_filtered_json;
if (is_null($_filtered_json)) {
$ok_elements = array('client_uuid','server_uuid','last_config','hostname');
$data = json_decode(file_get_contents('php://input'), true);
$_filtered_json = array_intersect_key($data,array_flip($ok_elements));
}
return $_filtered_json;
}
function my_uuid() {
global $config_db;
$uuid = $config_db->getProp(CONFIG_KEY,'uuid');
if (empty($uuid)) {
$uuid=trim(shell_exec('/usr/bin/uuidgen'));
$config_db->setProp(CONFIG_KEY,array('uuid'=>$uuid));
}
return $uuid;
}
function find_free_ipaddress() {
global $hosts_db, $dhcp_db;
$ip = ip2long($ip_str);
if ( ! $ip_str_start = $dhcp_db->getProp('lan0','DhcpRangeStart') ) {
return false;
}
if ( ! $ip_str_end = $dhcp_db->getProp('lan0','DhcpRangeEnd') ) {
return false;
}
$ip_start = ip2long($ip_str_start)+1;
$ip_end = ip2long($ip_str_end);
while($ip_start <= $ip_end){
$hosts = $hosts_db->getAllByProp('IpAddress',long2ip($ip_start));
if (count($hosts)==0) {
return long2ip($ip_start);
}
$start_ip++;
}
return false;
}
function ROUTE_POST_ping() {
global $config_db, $hosts_db;
$data = filtered_json();
$server_uuid=my_uuid();
$out=array();
$domainName = (string)$config_db->getKeyValue('DomainName');
if (!isset($data['server_uuid'])) {
# client is unmanaged, let's fix that
$systemName = (string)$config_db->getKeyValue('SystemName');
$TCPPort = $config_db->getProp(CONFIG_KEY,'TCPPort');
$out['ping_url']="http://$systemName.$domainName:$TCPPort/ping";
$out['server_uuid']=$server_uuid;
} elseif ($data['server_uuid'] != $server_uuid) {
# this client belongs to someone else, let's ignore it
echo json_encode(array('error'=>'you\'re devoted to another master'));
exit;
}
if (!isset($data['client_uuid'])) {
# client hasn't sent its uuid. This is not allowed (client uuid is always generated client-side)
echo json_encode(array('error'=>'missing client uuid'));
exit;
}
$client_ip = $_SERVER['REMOTE_ADDR'];
$client = false;
if (! $client = getClientFromUUID($data['client_uuid'])) {
# client uuid not registered, let's search for IP
$mac = getClientMac();
if ($client = getClientFromIP($client_ip)) {
# we have one
if (isset($client['uuid'])) {
# with a registered UUID, that we know to be different from ours
# so it's an IP collision.
# we investigate:
if ($mac == $client['MacAddress']) {
# the macaddress is the same, but we have different uuid
echo json_encode(array('error'=>'uuid mismatch'));
exit;
} elseif (!$mac) {
# we don't have any macaddress info (maybe because we're on
# different networks, so no arp)
echo json_encode(array('error'=>'can\'t add host because no macaddress'));
exit;
} else {
# we have a macadrress different from the one we have
# registered for this IP address, so it's a real IP collision
# and we add a new host reservation
if (! $new_ip = find_free_ipaddress()) {
echo json_encode(array('error'=>'no more free ip addresses'));
exit;
}
$props = array(
'Description' => '',
'IpAddress' => $new_ip,
'MacAddress' => $mac,
'uuid' => $data['client_uuid']
);
if (!$hosts_db->setKey($data['hostname'], 'local', $props)) {
echo json_encode(array('error'=>'can\'t add new host'));
exit;
}
$client = getClientFromUUID($data['client_uuid']);
}
} else {
# the configured client doen't have the uuid
# so we update it
$props = array(
'uuid' => $data['client_uuid']
);
if (!$hosts_db->setProp($client['name'], $props)) {
echo json_encode(array('error'=>'can\'t update host'));
exit;
}
$client['uuid'] = $data['client_uuid'];
}
} else {
# no client registered with this uuid or this IP
if ($mac) {
# we have a client mac address,
# we can register a new host
if (isset($data['hostname'])) {
$props = array(
'Description' => '',
'IpAddress' => $client_ip,
'MacAddress' => $mac,
'uuid' => $data['client_uuid']
);
if (!$hosts_db->setKey($data['hostname'], 'local', $props)) {
echo json_encode(array('error'=>'can\'t add host'));
exit;
}
$client = getClientFromUUID($data['client_uuid']);
} else {
echo json_encode(array('error'=>'missing client hostname'));
exit;
}
} else {
# we don't have a mac address (maybe a different network)
# we can't register a new host
echo json_encode(array('error'=>'missing client mac address'));
exit;
}
}
}
$hosts_db->setProp($client['name'],array('LastSeen' => time()));
if (isset($data['last_config'])) {
$hosts_db->setProp($client['name'],array('LastConfig' => $data['last_config']));
$current_config = $config_db->getProp(CONFIG_KEY,'lastConfig');
if ($data['last_config'] != $current_config ) {
exec('/usr/bin/sudo /sbin/e-smith/signal-event client-reconfigure ' . $client['name'] . ' > /dev/null &');
}
}
$out['hostname']=$client['name'];
$out['domainname']=$domainName;
$out['ipaddress']=$client['IpAddress'];
$out['server_uuid']=$server_uuid;
echo json_encode($out);
}