version 1.0;

/* ------------------------------------------------------------------
 * LICENSE
 * ------------------------------------------------------------------
 *
 * Copyright (c) 2013, Juniper Networks
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 *
 * (1) Redistributions of source code must retain the above copyright notice, this 
 * list of conditions and the following disclaimer. 
 *
 * (2) Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution. 
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of Juniper Networks.
 * 
 *
 * ------------------------------------------------------------------
 * AUTHORS AND CONTRIBUTORS
 * ------------------------------------------------------------------
 *
 * Jeremy Schulman, Juniper Networks
 * - initial release (https://github.com/jeremyschulman/jctyztp/)
 *
 * Frank Wall, noris network AG
 * - adapt for integration in The Foreman
 * - fix compatibility with older Junos releases
 * 
 *
 * ------------------------------------------------------------------
 * LIMITATIONS
 * ------------------------------------------------------------------
 *
 * To maintain backwards compatibility with JunOS 11.x (and maybe 
 * even 10.x) you MUST AVOID all of these:
 * - global variable $junos-context (introduced with Junos 11.1)
 * - mutable variables (introduced with JunOS 12.2 -> SLAX 1.1)
 * - native functions (introduced with JunOS 12.2 -> SLAX 1.1)
 *
 * ------------------------------------------------------------------
 */

/* ------------------------------------------------------------------ */
/* XML namespaces                                                     */
/* ------------------------------------------------------------------ */

/* Juniper */
ns junos = "http://xml.juniper.net/junos/*/junos";
ns xnm = "http://xml.juniper.net/xnm/1.1/xnm";
ns jcs = "http://xml.juniper.net/junos/commit-scripts/1.0";
ns ztp = "http://xml.juniper.net/junos/ztp";

/* EXSLT */
ns exsl extension = "http://exslt.org/common";
ns func extension = "http://exslt.org/functions";
ns str extension = "http://exslt.org/strings";

/* private namespace for this script */
ns jctyztp = "http://xml.juniper.com/jctyztp/1.0";
ns fmztp = "http://xml.juniper.com/fmztp/1.0";

/* depending on Junos version relative path may be broken */
/* import '../import/junos.xsl'; */
import '/usr/libdata/cscript/import/junos.xsl';

/* ------------------------------------------------------------------ */
/* Script parameters                                                  */
/* ------------------------------------------------------------------ */

param $server = 'ztpserver';
param $mediapath = 'url --url http://mirror.centos.org/centos/7/os/x86_64';

/* ------------------------------------------------------------------ */
/* Constants                                                          */
/* ------------------------------------------------------------------ */

var $APPNAME = 'foreman-ztp';
var $SYSLOG = 'user.info';
var $TMPDIR = '/var/tmp';
var $JUNOS_CONF = '/var/tmp/junos.conf';

var $ZTP_GROUP_NAME = "fmztp";
var $ZTP_MACRO_NAME = "conf";
var $ZTP_LOCKFILE = '/tmp/fmztp.lock';

/* ------------------------------------------------------------------ */
/* Global variables                                                   */
/* ------------------------------------------------------------------ */

/* Open a connection to the device API */
var $jnx = jcs:open();

/* ------------------------------------------------------------------ */
/*                                MAIN                                */ 
/* ------------------------------------------------------------------ */

match / {

  /* Terminate on connection error */
  if(not( $jnx )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to connect to Junos API");
    expr fmztp:terminate();
  }
  
  var $running = fmztp:only_once();
  if( $running ) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": start op script: already running, exiting..." );
    <xsl:message terminate="yes">;
  }
  
  /* if the $JUNOS_CONF file is not on the device, then */
  /* download it from the server */
  if(not( fmztp:file-exists( $JUNOS_CONF ))) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": obtaining device config file");
    var $cp = fmztp:dl_junos_conf();
  }
  
  /* now load $JUNOS_CONF into the candidate configuration so we can */
  /* extract the ZTP config */
  var $ztp_conf = fmztp:ld_junos_conf();
  var $has_version = $ztp_conf/has_version;
  var $new_package = $ztp_conf/package;
  expr jcs:syslog( $SYSLOG, $APPNAME, ": preparing update from ", $has_version, " to ", $new_package );
  
  /* if we have a version difference, then we will install the new OS */
  /* and reboot the device.  the $JUNOS_CONF file will be loaded on */
  /* after the install process completes */
  if( $ztp_conf/install ) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": Junos install required" );
    var $os = fmztp:install_os( $ztp_conf );
    expr jcs:syslog( $SYSLOG, $APPNAME, ": rebooting in 60 seconds" );
    expr jcs:syslog( $SYSLOG, $APPNAME, ": SCRIPT-END");
    expr fmztp:reboot( 1 );
    expr jcs:close( $jnx );
  }
  else {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": no Junos install required" );
    /* puppet agent is only supported on Junos 12.3R2.5 */
    if( jcs:regex( "12.3R2.5", $ztp_conf/has_version )) {
	expr jcs:syslog( $SYSLOG, $APPNAME, ": puppet agent install required" );
	var $puppet = fmztp:install_puppet( $ztp_conf );
    }
    else {
	expr jcs:syslog( $SYSLOG, $APPNAME, ": os version is ", $has_version, ", but puppet agent is only available on 12.3R2.5" );
    }
    var $fini = fmztp:finalize();
    expr jcs:syslog( $SYSLOG, $APPNAME, ": SCRIPT-END");
    expr jcs:close( $jnx );
  }
}

/* ------------------------------------------------------------------ */
/* HTTP Junos configuration file onto the device                      */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:dl_junos_conf">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": downloading new Junos conf...");
  expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", 'http://foreman.example.com/unattended/finish');

  var $get = <file-copy> {
    <source> 'http://foreman.example.com/unattended/finish';
    <destination> $JUNOS_CONF;
    <staging-directory> $TMPDIR;
  };

  var $got = jcs:execute( $jnx, $get );

  if(not( fmztp:file-exists( $JUNOS_CONF ))) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to find new Junos conf at ", $JUNOS_CONF);
    expr fmztp:terminate();
  }

  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Load the $JUNOS_CONF file into the candidate config and extract    */
/* the ZTP config from the [edit groups] area.  Do *NOT* commit       */
/* this configuration yet, since we may need to install the OS first  */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:ld_junos_conf">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": loading new Junos conf...");

  /* get the current version from the configuration file */
  
  var $get_cur_ver = <get-configuration database='committed'> { <configuration> { <version>; }};
  var $got_cur_ver = jcs:execute( $jnx, $get_cur_ver );

  /* now load the configuration file we got from the ztp server */
  var $do_load = <load-configuration action="override" url=$JUNOS_CONF format="text">;
  var $did_load = jcs:execute( $jnx, $do_load );
  if(not( $did_load/load-success )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to load config ", $JUNOS_CONF );
    expr fmztp:terminate();
  }
  
  expr jcs:syslog( $SYSLOG, $APPNAME, ": extracting Junos config parameters...");
  var $get = <get-configuration> { <configuration> {
    <version>;
    <groups> { <name> $ZTP_GROUP_NAME;
      <apply-macro> { <name> $ZTP_MACRO_NAME;
      }
    }
  }};
  
  var $got = jcs:execute( $jnx, $get );

  expr jcs:syslog( $SYSLOG, $APPNAME, ": package: ", $got//data[name = 'package']/value);
  expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", $mediapath);

  /* create a node-set to store the following elements             */
  /* has_version = current Junos version string                    */
  /* package = filename of Junos package (*.tgz)                   */
  /* mediapath = URL where package is obtained from                */
  /* install = present if a install is requeired                   */
  var $ver = $got_cur_ver/version;
  var $package = $got//data[name = 'package']/value;
  var $puppet = $got//data[name = 'puppet']/value;
  var $conf := {
    <has_version> $ver; 
    <package> $package;
    <puppet> $puppet;
    <url> $mediapath;
    if(not( jcs:regex( $ver, $package ))) {
      <install>;
    }
  }

  /* @@@ should put some trap here on ensuring that the config */
  /* @@@ file actually had the correct group/macro defined */
    
  <func:result select="$conf">;
}

/* ------------------------------------------------------------------ */
/* Junos Software Installation - download the software from the HTTP  */
/* server and perform the 'request system software add' operation     */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:install_os">
{
  param $ztp_conf;

  var $local_image = $TMPDIR _ "/" _ $ztp_conf/package;
  
  if( fmztp:file-exists( $local_image )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": junos image exists, no download needed" );
  }
  else {
  
    /* request system storage cleanup */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": cleaning filesystem" );
    var $clean = jcs:execute( $jnx, 'request-system-storage-cleanup' ); 

    /* file copy .... */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": downloading junos image..." );    
    expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", $ztp_conf/url, $ztp_conf/package );
    var $do_copy = <file-copy> {
      <source> $ztp_conf/url _ $ztp_conf/package;
      <destination> $TMPDIR;
      <staging-directory> $TMPDIR;
    };
    var $did_copy = jcs:execute( $jnx, $do_copy );
    
    /* trap error here */
    if( not(fmztp:file-exists( $local_image )) ) {
        expr jcs:syslog( $SYSLOG, $APPNAME, ": ERROR: unable to download junos image" );
        expr fmztp:terminate();
    }
  }
  
  /* request system software add ... */
  expr jcs:syslog( $SYSLOG, $APPNAME, ": installing junos image" );    
  var $do_install = <request-package-add> {
    <no-validate>;
    <package-name> $local_image;
  };
  var $did_install = jcs:execute( $jnx, $do_install );
  /* @@@ need to trap error here on $did_install */
  
  expr jcs:syslog( $SYSLOG, $APPNAME, ": completed installing junos image" );
   
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Reboot the device given a delay, in minutes                        */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:reboot">
{
  param $in_min;

  var $do_reboot = <request-reboot> { <in> $in_min; };
  var $did_reboot = jcs:execute( $jnx, $do_reboot );
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Puppet Agent Installation - download the software from the HTTP    */
/* server and perform the 'request system software add' operation     */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:install_puppet">
{
  param $ztp_conf;
  expr jcs:syslog( $SYSLOG, $APPNAME, ": starting puppet installation..." );

  if (fmztp:is-installed("puppet")) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package puppet is already installed" );
    <func:result select="true()">;
  }

  var $local_image = $TMPDIR _ "/" _ $ztp_conf/puppet;
  
  if( fmztp:file-exists( $local_image )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package puppet exists, no download needed" );
  }
  else {
  
    /* request system storage cleanup */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": cleaning filesystem" );
    var $clean = jcs:execute( $jnx, 'request-system-storage-cleanup' ); 

    /* file copy .... */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": downloading puppet image..." );    
    expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", $ztp_conf/url, $ztp_conf/puppet );    
    var $do_copy = <file-copy> {
      <source> $ztp_conf/url _ $ztp_conf/puppet;
      <destination> $TMPDIR;
      <staging-directory> $TMPDIR;
    };
    var $did_copy = jcs:execute( $jnx, $do_copy );
    
    /* trap error on $did_copy */
    if( not(fmztp:file-exists( $local_image )) ) {
      expr jcs:syslog( $SYSLOG, $APPNAME, ": ERROR: failed to download puppet image" );
      expr fmztp:terminate();
    }
  }

  /* request system software add ... */
  expr jcs:syslog( $SYSLOG, $APPNAME, ": installing puppet image" );    
  var $do_install = <request-package-add> {
    <no-validate>;
    <package-name> $local_image;
  };
  var $did_install = jcs:execute( $jnx, $do_install );
  
  /* NOTE: To complete the puppet installation you need to take manual steps, see:
   *       http://www.juniper.net/techpubs/en_US/release-independent/junos-puppet/information-products/pathway-pages/index.html
   */

  /* validate installation */
  if (not(fmztp:is-installed("puppet"))) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": ERROR: failed to install package puppet" );
    expr fmztp:terminate();
  }

  expr jcs:syslog( $SYSLOG, $APPNAME, ": completed installing puppet image" );
   
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Finalize the ZTP process; i.e. after the OS is correct.  Remove    */
/* the $JUNOS_CONF file and committing the configuration to make      */
/* it active.                                                         */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:finalize">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": deleting temp config file...");  
  var $rm1 = fmztp:file-delete( $JUNOS_CONF );

  expr jcs:syslog( $SYSLOG, $APPNAME, ": sending finish signal to foreman...");  
  var $do_foreman = <file-copy> {
      <source> 'http://foreman.example.com/unattended/provision';
      <destination> "/tmp";
      <staging-directory> "/tmp";
  };
  var $did_foreman = jcs:execute( $jnx, $do_foreman );

  expr jcs:syslog( $SYSLOG, $APPNAME, ": deleting ztp lock file...");  
  var $rm2 = fmztp:file-delete( $ZTP_LOCKFILE );

  /* commit the configuration that was previously loaded */  
  expr jcs:syslog( $SYSLOG, $APPNAME, ": committing configuration...");  
  var $commit = jcs:execute( $jnx, 'commit-configuration' );
  if( $commit//self::xnm:error ) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to commit configuration: ", $commit//self::xnm:error/message );
    var $die = fmztp:terminate();
  }

  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Helper routine: check to see if a file exists on the device,       */
/* returns [ true | false ]                                           */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:file-exists">
{
  param $filename;
  var $ls_file = <file-list> { <path> $filename; };
  var $ls_got = jcs:execute( $jnx, $ls_file );
  var $retval = boolean( $ls_got//file-information );

  <func:result select="$retval">;
}

<func:function name="fmztp:file-delete">
{
  param $filename;
  var $do_rm = <file-delete> { <path> $filename; };
  var $did_rm = jcs:execute( $jnx, $do_rm );
  /* @@@ trap error */
  
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Helper routine: create a lockfile to make sure the script only     */
/* runs once, and terminate if it is already running.                 */
/* returns [ true | false ]                                           */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:only_once">
{
  if( fmztp:file-exists( $ZTP_LOCKFILE )) {
    <func:result select="true()">;
  }
  else {
    var $do_lock = <file-put> {
      <filename> $ZTP_LOCKFILE;
      <encoding> 'ascii';
      <file-contents> 'locked';
    };
    var $did_lock = jcs:execute( $jnx, $do_lock );
    <func:result select="false()">;
  }
}

<func:function name="fmztp:terminate">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": SCRIPT-FAILED" );
  var $rm_lock = fmztp:file-delete( $ZTP_LOCKFILE );
  <xsl:message terminate="yes">;
}

/* ------------------------------------------------- */
/* check if software package is installed            */
/* ------------------------------------------------- */
<func:function name="fmztp:is-installed">
{
  param $string;

  var $do_query = <get-software-information>;
  var $did_query = jcs:execute( $jnx, $do_query );

  if( jcs:regex( $string, $did_query )) {
    expr jcs:output("package found: ", $string);
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package found: ", $string);
    <func:result select="true()">;
  }
  else {
    expr jcs:output("package NOT found: ", $string);
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package NOT found: ", $string);
    <func:result select="false()">;
  }
}
