diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..60e24c2 --- /dev/null +++ b/.drone.yml @@ -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 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9dc64a6 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6e99c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.sh +*.tar.* +*.rpm +*.log diff --git a/README.md b/README.md index 65f6a74..a3e78a9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/createlinks.pl b/createlinks.pl new file mode 100644 index 0000000..a2f816b --- /dev/null +++ b/createlinks.pl @@ -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); diff --git a/libreschool-client-manager.spec b/libreschool-client-manager.spec new file mode 100644 index 0000000..682d3bd --- /dev/null +++ b/libreschool-client-manager.spec @@ -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 - 1.0-1.ns7 +- First release for NethServer 7 diff --git a/root/etc/ansible/ansible-libreschool.cfg b/root/etc/ansible/ansible-libreschool.cfg new file mode 100644 index 0000000..b1a27c0 --- /dev/null +++ b/root/etc/ansible/ansible-libreschool.cfg @@ -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 diff --git a/root/etc/ansible/inventory/clients.pl b/root/etc/ansible/inventory/clients.pl new file mode 100755 index 0000000..d9a30ea --- /dev/null +++ b/root/etc/ansible/inventory/clients.pl @@ -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; diff --git a/root/etc/ansible/library/e-smith b/root/etc/ansible/library/e-smith new file mode 100644 index 0000000..4301fe0 --- /dev/null +++ b/root/etc/ansible/library/e-smith @@ -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; diff --git a/root/etc/ansible/roles/common/tasks/main.yml b/root/etc/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000..9ff1122 --- /dev/null +++ b/root/etc/ansible/roles/common/tasks/main.yml @@ -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 diff --git a/root/etc/ansible/roles/update-libreschool-client/handlers/main.yml b/root/etc/ansible/roles/update-libreschool-client/handlers/main.yml new file mode 100644 index 0000000..1bd5d0e --- /dev/null +++ b/root/etc/ansible/roles/update-libreschool-client/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: rerun-libreschool-client + shell: "(/usr/sbin/libreschool-client & )" + ignore_errors: yes diff --git a/root/etc/ansible/roles/update-libreschool-client/tasks/main.yml b/root/etc/ansible/roles/update-libreschool-client/tasks/main.yml new file mode 100644 index 0000000..5ff0a13 --- /dev/null +++ b/root/etc/ansible/roles/update-libreschool-client/tasks/main.yml @@ -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 diff --git a/root/etc/cron.d/libreschool-client-manager b/root/etc/cron.d/libreschool-client-manager new file mode 100644 index 0000000..e3ff18a --- /dev/null +++ b/root/etc/cron.d/libreschool-client-manager @@ -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 diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/ManagementKeyFile b/root/etc/e-smith/db/configuration/defaults/client-manager/ManagementKeyFile new file mode 100644 index 0000000..a2d5166 --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/ManagementKeyFile @@ -0,0 +1 @@ +/home/amgmt/.ssh/id_rsa.pub diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/ManagementPrivateKeyFile b/root/etc/e-smith/db/configuration/defaults/client-manager/ManagementPrivateKeyFile new file mode 100644 index 0000000..5b4ee9d --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/ManagementPrivateKeyFile @@ -0,0 +1 @@ +/home/amgmt/.ssh/id_rsa diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/NamingBase b/root/etc/e-smith/db/configuration/defaults/client-manager/NamingBase new file mode 100644 index 0000000..10441b4 --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/NamingBase @@ -0,0 +1 @@ +lab diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/NamingDigits b/root/etc/e-smith/db/configuration/defaults/client-manager/NamingDigits new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/NamingDigits @@ -0,0 +1 @@ +2 diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/TCPPort b/root/etc/e-smith/db/configuration/defaults/client-manager/TCPPort new file mode 100644 index 0000000..13de30f --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/TCPPort @@ -0,0 +1 @@ +3000 diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/access b/root/etc/e-smith/db/configuration/defaults/client-manager/access new file mode 100644 index 0000000..a5b73ed --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/access @@ -0,0 +1 @@ +green diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/lastConfig b/root/etc/e-smith/db/configuration/defaults/client-manager/lastConfig new file mode 100644 index 0000000..573541a --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/lastConfig @@ -0,0 +1 @@ +0 diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/status b/root/etc/e-smith/db/configuration/defaults/client-manager/status new file mode 100644 index 0000000..86981e6 --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/status @@ -0,0 +1 @@ +enabled diff --git a/root/etc/e-smith/db/configuration/defaults/client-manager/type b/root/etc/e-smith/db/configuration/defaults/client-manager/type new file mode 100644 index 0000000..24e1098 --- /dev/null +++ b/root/etc/e-smith/db/configuration/defaults/client-manager/type @@ -0,0 +1 @@ +service diff --git a/root/etc/e-smith/events/actions/client-adopt b/root/etc/e-smith/events/actions/client-adopt new file mode 100644 index 0000000..15b9982 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-adopt @@ -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 diff --git a/root/etc/e-smith/events/actions/client-reboot b/root/etc/e-smith/events/actions/client-reboot new file mode 100755 index 0000000..310c26a --- /dev/null +++ b/root/etc/e-smith/events/actions/client-reboot @@ -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"; + +} diff --git a/root/etc/e-smith/events/actions/client-reboot-all b/root/etc/e-smith/events/actions/client-reboot-all new file mode 100755 index 0000000..4121f47 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-reboot-all @@ -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; diff --git a/root/etc/e-smith/events/actions/client-reconfigure b/root/etc/e-smith/events/actions/client-reconfigure new file mode 100755 index 0000000..2a89c41 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-reconfigure @@ -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"; + +} diff --git a/root/etc/e-smith/events/actions/client-reconfigure-all b/root/etc/e-smith/events/actions/client-reconfigure-all new file mode 100755 index 0000000..c64a320 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-reconfigure-all @@ -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 diff --git a/root/etc/e-smith/events/actions/client-shutdown b/root/etc/e-smith/events/actions/client-shutdown new file mode 100755 index 0000000..ce23fb7 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-shutdown @@ -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"; + +} diff --git a/root/etc/e-smith/events/actions/client-shutdown-all b/root/etc/e-smith/events/actions/client-shutdown-all new file mode 100755 index 0000000..d6add01 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-shutdown-all @@ -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; diff --git a/root/etc/e-smith/events/actions/client-update-lastconfig b/root/etc/e-smith/events/actions/client-update-lastconfig new file mode 100755 index 0000000..3c01a62 --- /dev/null +++ b/root/etc/e-smith/events/actions/client-update-lastconfig @@ -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}" diff --git a/root/etc/e-smith/events/actions/client-wake b/root/etc/e-smith/events/actions/client-wake new file mode 100755 index 0000000..88b45fc --- /dev/null +++ b/root/etc/e-smith/events/actions/client-wake @@ -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"; + +} diff --git a/root/etc/e-smith/events/actions/client-wake-all b/root/etc/e-smith/events/actions/client-wake-all new file mode 100755 index 0000000..73a34bb --- /dev/null +++ b/root/etc/e-smith/events/actions/client-wake-all @@ -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; diff --git a/root/etc/e-smith/templates/etc/ansible/reconfigure_clients.yml/00_common_role b/root/etc/e-smith/templates/etc/ansible/reconfigure_clients.yml/00_common_role new file mode 100644 index 0000000..fd89d42 --- /dev/null +++ b/root/etc/e-smith/templates/etc/ansible/reconfigure_clients.yml/00_common_role @@ -0,0 +1,7 @@ +--- + +- hosts: all + user: root + + roles: + - common diff --git a/root/etc/e-smith/templates/etc/ansible/reconfigure_clients.yml/99_last_role b/root/etc/e-smith/templates/etc/ansible/reconfigure_clients.yml/99_last_role new file mode 100644 index 0000000..79d0e1f --- /dev/null +++ b/root/etc/e-smith/templates/etc/ansible/reconfigure_clients.yml/99_last_role @@ -0,0 +1 @@ + - update-libreschool-client diff --git a/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/00template_defaults b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/00template_defaults new file mode 100644 index 0000000..5aa6397 --- /dev/null +++ b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/00template_defaults @@ -0,0 +1,10 @@ +{ + # + # 00template_defaults + # + + $localAccess = ''; + $port = ${'client-manager'}{TCPPort} || "3000"; + + ""; +} diff --git a/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/20Manager b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/20Manager new file mode 100644 index 0000000..581cf04 --- /dev/null +++ b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/20Manager @@ -0,0 +1,46 @@ +{ + + $OUT .= <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 +} diff --git a/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/40DefaultAccess b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/40DefaultAccess new file mode 100644 index 0000000..da228a2 --- /dev/null +++ b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/40DefaultAccess @@ -0,0 +1,7 @@ + + Options None + AllowOverride None + order deny,allow + deny from all + allow from none + diff --git a/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/60WebInterface b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/60WebInterface new file mode 100644 index 0000000..fa511f0 --- /dev/null +++ b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/60WebInterface @@ -0,0 +1,16 @@ + +DocumentRoot /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 + diff --git a/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/80Mime b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/80Mime new file mode 100644 index 0000000..08c450a --- /dev/null +++ b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/80Mime @@ -0,0 +1,4 @@ +AddDefaultCharset UTF-8 + + TypesConfig /etc/mime.types + diff --git a/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/90include b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/90include new file mode 100644 index 0000000..e7f3cdb --- /dev/null +++ b/root/etc/e-smith/templates/etc/httpd/client-manager/httpd.conf/90include @@ -0,0 +1 @@ +Include conf.d/php.conf diff --git a/root/etc/e-smith/templates/etc/sudoers/50client-manager b/root/etc/e-smith/templates/etc/sudoers/50client-manager new file mode 100644 index 0000000..d178511 --- /dev/null +++ b/root/etc/e-smith/templates/etc/sudoers/50client-manager @@ -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; + +} diff --git a/root/etc/logrotate.d/client-manager b/root/etc/logrotate.d/client-manager new file mode 100644 index 0000000..2aff453 --- /dev/null +++ b/root/etc/logrotate.d/client-manager @@ -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 +} diff --git a/root/etc/sudoers.d/amgmt b/root/etc/sudoers.d/amgmt new file mode 100644 index 0000000..9277545 --- /dev/null +++ b/root/etc/sudoers.d/amgmt @@ -0,0 +1 @@ +amgmt ALL=(ALL:ALL) NOPASSWD:ALL \ No newline at end of file diff --git a/root/etc/sysconfig/client-manager b/root/etc/sysconfig/client-manager new file mode 100644 index 0000000..e6cf468 --- /dev/null +++ b/root/etc/sysconfig/client-manager @@ -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 diff --git a/root/usr/lib/systemd/system/client-manager-reload.service b/root/usr/lib/systemd/system/client-manager-reload.service new file mode 100644 index 0000000..412aa22 --- /dev/null +++ b/root/usr/lib/systemd/system/client-manager-reload.service @@ -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 diff --git a/root/usr/lib/systemd/system/client-manager-reload.timer b/root/usr/lib/systemd/system/client-manager-reload.timer new file mode 100644 index 0000000..cbabd21 --- /dev/null +++ b/root/usr/lib/systemd/system/client-manager-reload.timer @@ -0,0 +1,6 @@ +[Unit] +Description=Delayed graceful restart of client-manager +Conflicts=client-manager-reload.service + +[Timer] +OnActiveSec=5 diff --git a/root/usr/lib/systemd/system/client-manager.service b/root/usr/lib/systemd/system/client-manager.service new file mode 100644 index 0000000..2413aad --- /dev/null +++ b/root/usr/lib/systemd/system/client-manager.service @@ -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 diff --git a/root/usr/share/client-manager/EsmithDatabase.php b/root/usr/share/client-manager/EsmithDatabase.php new file mode 100644 index 0000000..e0e0268 --- /dev/null +++ b/root/usr/share/client-manager/EsmithDatabase.php @@ -0,0 +1,391 @@ +. + */ + +/** + * 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: + * + * Cmnd_Alias SME = /sbin/e-smith/db, /sbin/e-smith/signal-event + * www ALL=NOPASSWD: SME + * + * + * + * @author Giacomo Sanchietti + * @author Davide Principi + * @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; + } + + + + +} \ No newline at end of file diff --git a/root/usr/share/client-manager/index.php b/root/usr/share/client-manager/index.php new file mode 100644 index 0000000..f4015fc --- /dev/null +++ b/root/usr/share/client-manager/index.php @@ -0,0 +1,253 @@ +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); +}