Virtualmin utility scripts

The college’s neongrit linux server uses Virtualmin/Usermin to handle the admin of users, and providing those users with a web interface to configure things like how apache works for their virtual host, creating new databases, etc.

It’s a very useful framework, and has most of the essentials we needed, but I wrote a few scripts in Perl to assist me with some frequent tasks, the way that fit me- which is perhaps more true of sysadmin scripts than any other sort of software, that they are built to scratch an itch. Here’s one to assist batch adding of users and creating them a virtual web host.

Adding users en masse

This script is invoked against a file containing the names and student IDs in various different formats, one per line. I simply uncommented the regular expression that matched what I had extracted from some system or other (electronic register, spreadsheet or the like). I have some custom fields set up for the cohort and the expiry date, mainly to alert me when accounts need deleting.

It also generates a random password, and a concatenated PDF file to print off and give to the students in class as a bootstrap document (username, password, link to the neongrit user’s guide, etc.).

#!/usr/bin/perl -w
#
# makeuser.pl
#
# This script makes new users, with web hosting and an initial database, and pre-installs
# phpMyAdmin into their ~/public_html
# 
# Switches are:
# -r / --random   for randomly generated passwords. You must ask to save output!
# -t / --test     for a test run. No actual commands are run.
# -p / --pdf      for PDF output. Otherwise you get CSV, for feeding into Excel or something.
# -s / --staff    for staff (omits the s in front of the id, and sets staff field to "yes")
# -o / --output   the filename of the results file (if PDF has been requested)
# -c / --cohort   the value for the cohort custom field
# -e / --expiry   the value for the expiry custom field
#
# Chances are, if enrolling students, you want a command line like this:
# sudo ./makeusers.pl -r -p -o name_of_pdf.pdf -c "cohort name" -e "expiry date" < file_with_students.txt

use strict;
use Getopt::Long;
use Math::Random::ISAAC;
use vars qw($seed @seeds $rng %font $pdf %opts);
use constant mm => 25.4 / 72;
use constant in => 1 / 72;
use constant pt => 1;
use constant REGEXP => /^(\d+) (.+)$/;        #123456 Surname, Lastname
  #/([^,]+),"([^"]*)"/;   123456,"Surname, Lastname"
  #/(\d+),"([^"]*)"/;     123456,"Surname, Lastname"
  #/(\d+)\s+(.*)\s*$/;    123456    Surname, Lastname   
  #/(\d+)\s+"([^"]*)"/;   123456   "Surname, Lastname"

sub get_data();

%opts = (staff => 0,
         random => 0,
         test => 0,
         output => 'out.pdf',
         cohort => '',
         expiry => '');

GetOptions(\%opts, 'staff|s', 'random|r', 'pdf|p', 'output|o=s', 'test|t', 'cohort|c=s', 'expiry|e=s');

if ($opts{staff}) { $opts{staff} = ''; } else { $opts{staff} = 's'; }

my @paras = get_data;

if ($opts{pdf}) {
  use PDF::API2;
  $pdf = PDF::API2->new( -file => $opts{output} );
  %font = (
     Helvetica => {
         Bold   => $pdf->corefont( 'Helvetica-Bold',    -encoding => 'latin1' ),
         Roman  => $pdf->corefont( 'Helvetica',         -encoding => 'latin1' ),
         Italic => $pdf->corefont( 'Helvetica-Oblique', -encoding => 'latin1' ),
     },
     DejaVuSans => {
         Bold  => $pdf->ttfont('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', -encoding => 'utf8'),
         Roman => $pdf->ttfont('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', -encoding => 'utf8')
     },
  );
} elsif ($opts{output}) {
    open(CSV, '>' . $opts{output});
}



if ($opts{random}) {
  open(RAND, '<:bytes', '/dev/random') or die("Can't open /dev/random!\n");
  print STDERR "Acquiring entropy...";
  read(RAND, $seed, 4);
  push(@seeds, unpack('V', $seed));
  close(RAND);
  $rng = Math::Random::ISAAC->new(@seeds);
  print STDERR "done.\n";
}

sub mkpw () {
   my @alpha = qw(a b c d e f - g h i j k m n ! o p q r s t u - v w x y z A B + C D E F G H J K L M N P . Q R T U V W X Y Z . 2 3 4 5 6 7 8 9 - . + ^ * !);
   my $pw = '';
   for (my $x = 0; $x < 8; $x++) {
      $pw .= $alpha[int($rng->rand() * @alpha)];
    }
    return $pw;
}

open(FILE, shift || "-");
while(<FILE>) {
  chomp($_);
  $_ =~ REGEXP;
  my $id=$2;
  my $name=$1;

  my $pw = $opts{random} ? mkpw() : 'bristol';

  my $cmd = 'virtualmin create-domain --domain '. $opts{staff} . $id . '.neongrit.net --pass \'' . $pw . '\' --desc "' . $name . '" --unix --dir --webmin --web --mysql --limits-from-plan --plan "standard_student"'
            . ' --field-staff ' . ($opts{staff} ? '"no"' : '"yes"') . ($opts{cohort} ? ' --field-cohort "' . $opts{cohort}  . '"' : '') . ($opts{expiry} ? ' --field-expiry "' . $opts{expiry}  . '"' : '');
   if ($opts{test}) {
     print STDERR "------TESTMODE------\n\n$cmd\n\n";
   } else {
     print STDERR "$cmd\n\n";
     system($cmd);
   }
    if ($opts{random}) {
      if ($opts{pdf}) {
        my @hi = ({'g' => "Guten Tag", 'l' => 'German'},
		  {'g' => "Mabuhay", 'l' => 'Tagalog'},
		  {'g' => "Hello", 'l' => 'English'},
		  {'g' => "Bonjour", 'l' => 'French'},
		  {'g' => "Salut", 'l' => 'French'},
		  {'g' => "Hola", 'l' => 'Spanish'},
		  {'g' => "Hej", 'l' => 'Danish'},
		  {'g' => "Cze\x{015B}\x{0107}", 'l' => 'Polish'},
		  {'g' => "Dia dhuit", 'l' => 'Gaelic'},
		  {'g' => "Namaste", 'l' => 'Hindi'},
		  {'g' => "\x{0393}\x{03B5}\x{03B9}\x{03AC}", 'l' => 'Greek'},
		  {'g' => "0110100001100101011011000110110001101111", 'l' => 'UTF-8'},
		  {'g' => "Sul sul", 'l' => 'Simlish'},
		  {'g' => "Aloha", 'l' => 'Hawaiian'},
		  {'g' => "Ciao", 'l' => 'Italian'});
        my $page = $pdf->page;
        $page->mediabox (210/mm, 297/mm);
        $page->cropbox  (7.5/mm, 7.5/mm, 202.5/mm, 289.5/mm);
        my $headline_text = $page->text;
        $headline_text->font( $font{'DejaVuSans'}{'Bold'}, 36/pt );
        $headline_text->fillcolor('black');
        $headline_text->translate( 190/mm, 261/mm );
        $headline_text->text_right("neongrit");
        my $body_text = $page->text;
        $body_text->font($font{'DejaVuSans'}{'Roman'}, 11/pt );
	$body_text->fillcolor('black');
        my $rand = rand(@hi);
        my ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  $hi[$rand]{'g'} . '*',
	  -x        => 20/mm,
	  -y        => 245/mm,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  $paras[0],
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        my $ypos2 = $ypos;
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  "account for:\nlogin:\npassword:",
	  -x        => 40/mm,
	  -y        => $ypos2 - 14/pt,
	  -w        => 30/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'right',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  "$name\n$opts{staff}$id\n$pw",
	  -x        => 75/mm,
	  -y        => $ypos2 - 14/pt,
	  -w        => 100/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  $paras[1],
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  "http://$opts{staff}$id.neongrit.net/",
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'center',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  $paras[2],
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  $paras[3],
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'center',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  $paras[4],
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        $body_text->font($font{'DejaVuSans'}{'Roman'}, 8/pt );
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text,
	  "* " . $hi[$rand]{'l'} . " for 'Hello'",
	  -x        => 20/mm,
	  -y        => 20/mm,
	  -w        => 165/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'right',
	);

        my $page2 = $pdf->page;
        $page2->mediabox (210/mm, 297/mm);
        $page2->cropbox  (7.5/mm, 7.5/mm, 202.5/mm, 289.5/mm);
        my $body_text2 = $page2->text;
        $body_text2->font($font{'DejaVuSans'}{'Roman'}, 11/pt );
	$body_text2->fillcolor('black');
        my ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text2,
	  $paras[5],
	  -x        => 20/mm,
	  -y        => 265/mm,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text2,
	  "https://$opts{staff}$id.neongrit.net:10000/",
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'center',
	);
        ( $endw, $ypos, $paragraph ) = text_block(
	  $body_text2,
	  $paras[6],
	  -x        => 20/mm,
	  -y        => $ypos - 14/pt,
	  -w        => 170/mm,
	  -h        => 110/mm - 7/pt,
	  -lead     => 14/pt,
	  -parspace => 0/pt,
	  -align    => 'left',
	);

      } else {
        print CSV "\"$opts{staff}$id\", \"$name\", \"$pw\"\n";
      }
    }
  }

close(FILE);
if ($opts{pdf}) { $pdf->save; $pdf->end(); }

sub text_block {
    my $endw;
    my $text_object = shift;
    my $text        = shift;

    my %arg = @_;

    # Get the text in paragraphs
    my @paragraphs = split( /\n/, $text );

    # calculate width of all words
    my $space_width = $text_object->advancewidth(' ');

    my @words = split( /\s+/, $text );
    my %width = ();
    foreach (@words) {
        next if exists $width{$_};
        $width{$_} = $text_object->advancewidth($_);
    }

    my $ypos = $arg{'-y'};
    my @paragraph = split( / /, shift(@paragraphs) );

    my $first_line      = 1;
    my $first_paragraph = 1;

    # while we can add another line

    while ( $ypos >= $arg{'-y'} - $arg{'-h'} + $arg{'-lead'} ) {

        unless (@paragraph) {
            last unless scalar @paragraphs;

            @paragraph = split( / /, shift(@paragraphs) );

            $ypos -= $arg{'-parspace'} if $arg{'-parspace'};
            last unless $ypos >= $arg{'-y'} - $arg{'-h'};

            $first_line      = 1;
            $first_paragraph = 0;
        }

        my $xpos = $arg{'-x'};

        # while there's room on the line, add another word
        my @line = ();

        my $line_width = 0;
        if ( $first_line && exists $arg{'-hang'} ) {

            my $hang_width = $text_object->advancewidth( $arg{'-hang'} );

            $text_object->translate( $xpos, $ypos );
            $text_object->text( $arg{'-hang'} );

            $xpos       += $hang_width;
            $line_width += $hang_width;
            $arg{'-indent'} += $hang_width if $first_paragraph;

        }
        elsif ( $first_line && exists $arg{'-flindent'} ) {

            $xpos       += $arg{'-flindent'};
            $line_width += $arg{'-flindent'};

        }
        elsif ( $first_paragraph && exists $arg{'-fpindent'} ) {

            $xpos       += $arg{'-fpindent'};
            $line_width += $arg{'-fpindent'};

        }
        elsif ( exists $arg{'-indent'} ) {

            $xpos       += $arg{'-indent'};
            $line_width += $arg{'-indent'};

        }

        while ( @paragraph
            and $line_width + ( scalar(@line) * $space_width ) +
            $width{ $paragraph[0] } < $arg{'-w'} )
        {

            $line_width += $width{ $paragraph[0] };
            push( @line, shift(@paragraph) );

        }

        # calculate the space width
        my ( $wordspace, $align );
        if ( $arg{'-align'} eq 'fulljustify'
            or ( $arg{'-align'} eq 'justify' and @paragraph ) )
        {

            if ( scalar(@line) == 1 ) {
                @line = split( //, $line[0] );

            }
            $wordspace = ( $arg{'-w'} - $line_width ) / ( scalar(@line) - 1 );

            $align = 'justify';
        }
        else {
            $align = ( $arg{'-align'} eq 'justify' ) ? 'left' : $arg{'-align'};

            $wordspace = $space_width;
        }
        $line_width += $wordspace * ( scalar(@line) - 1 );

        if ( $align eq 'justify' ) {
            foreach my $word (@line) {

                $text_object->translate( $xpos, $ypos );
                $text_object->text($word);

                $xpos += ( $width{$word} + $wordspace ) if (@line);

            }
            $endw = $arg{'-w'};
        }
        else {

            # calculate the left hand position of the line
            if ( $align eq 'right' ) {
                $xpos += $arg{'-w'} - $line_width;

            }
            elsif ( $align eq 'center' ) {
                $xpos += ( $arg{'-w'} / 2 ) - ( $line_width / 2 );

            }

            # render the line
            $text_object->translate( $xpos, $ypos );

            $endw = $text_object->text( join( ' ', @line ) );

        }
        $ypos -= $arg{'-lead'};
        $first_line = 0;

    }
    unshift( @paragraphs, join( ' ', @paragraph ) ) if scalar(@paragraph);

    return ( $endw, $ypos, join( "\n", @paragraphs ) )

}

sub get_data() {
  (
    qq|Here are the details of your new account on the CoBC Computing Department's webserver. We hope you will put this to good use, sharpening your skills, whatever they are- be it relational databases using MySQL, web page development using HTML/CSS/JavaScript/SVG/PHP etc., or simply showcasing your creative work.\n\nIf you are reading this, it means your account is already set up to be made use of. Your account will be closed, and the contents deleted, approximately one month after the end of the course you are enrolled on, so make sure you don't keep the only copy of your work online!\n\nBelow are your logon credentials. Note that the password is case-sensitive. An upper case M is not the same as a lower case m.|,
    qq|Your website- which consists of a single placeholder page right now -can be found here:|,
    qq|Of course, you're going to want to change that, and upload many other files besides. To do that, you'll need a client program that understands the SFTP protocol.
If you are using Notepad++ or Dreamweaver to create your pages, you're in luck, as they both speak SFTP. For more full-featured file management you could do a lot worse than WinSCP. You can easily upload whole directories with that, and it installs to a USB memory stick or network drive. The latest version of a guide to using these tools can be found here:|,
    qq|http://pmb.neongrit.net/neongrit_guide.pdf|,
    qq|Please remember, the files you upload to this account are visible to anyone in the world with an internet connection. Just as with any other college IT resource, you are required to abide by the college's Acceptible Use Policy. Violations of the policy are treated very seriously and could result in your being withdrawn from your course.|,
    qq|Should you wish to change your password, check your quota (as of right now, you have 250MB of room), or add an extra database, you'll need to access Webmin, the web-based administration tool. If you don't, not to worry- it isn't the friendliest of interfaces. It can be found here:|,
    qq|Note that s in https. Not optional. Your web browser will give you a stern warning the first time you connect to Webmin. It's fine to ignore it in this case. Tell your browser that you're cool with it. With Firefox, click on \x{201C}I Understand the Risks\x{201D}, then \x{201C}Add exception...\x{201D}\n\nIf you have any problems, make your lecturer who handed you this sheet your first port of call.\n\n\nRegards,\n\n\nPhil\nphil.bambridge\@cityofbristol.ac.uk|
  );
}

Leave a Reply

Your email address will not be published. Required fields are marked *