Post on 16-Jan-2017
Mojo – TL;DRSimple REST Server
Hendrik Van BelleghemHendrik.vanbelleghem@gmail.com
2016 Perl Mongers Vlaanderen Meetup
Intro
• REST is typically HTTP based• Not standardized• Popularly used for APIs• Outputs XML, HTML, JSON• Verbs include GET, POST, DELETE & PUT• • https://en.wikipedia.org/wiki/Representational_state_transfer
Intro - REST REST typically uses standard HTTP calls: Specific entry: GET /User/name/<USER> Specific entry: GET /User/id/<ID> All Entries: GET /User Update entry: PUT /User Create entry: POST /User Delete entry: DELETE /User/name/<USER> Delete entry: DELETE /User/id/<ID>
Demo
• Cisco Secure ACS Server is used for Identity Management• API access to user accounts, devices..
• Mojolicious does REST just fine• Non-plugin approach• Proof of Concept / Users only
Mojo
• Next generation Web: Websockets, non-blocking, HTTP/1.1
• Web in a box: Development server & production server included
• Light-weight: No dependencies• Flexible routing• … just like Catalyst• DBIX::Class plugs in easily• … just like Catalyst• Done by the original Catalyst people
See http://www.mojolicious.org/
Mojo
Generate application skeleton:
# mojo generate app Net::Cisco::ACS::Mock
Output [mkdir] /home/hendrik/net_cisco_acsmock/script [write] /home/hendrik/net_cisco_acsmock/script/net_cisco_acsmock [chmod] /home/hendrik/net_cisco_acsmock/script/net_cisco_acsmock 744 [mkdir] /home/hendrik/net_cisco_acsmock/lib/Net/Cisco/ACS [write] /home/hendrik/net_cisco_acsmock/lib/Net/Cisco/ACS/Mock.pm [mkdir] /home/hendrik/net_cisco_acsmock/lib/Net/Cisco/ACS/Mock/Controller [write] /home/hendrik/net_cisco_acsmock/lib/Net/Cisco/ACS/Mock/Controller/Example.pm [mkdir] /home/hendrik/net_cisco_acsmock/t [write] /home/hendrik/net_cisco_acsmock/t/basic.t [mkdir] /home/hendrik/net_cisco_acsmock/public [write] /home/hendrik/net_cisco_acsmock/public/index.html [mkdir] /home/hendrik/net_cisco_acsmock/templates/layouts [write] /home/hendrik/net_cisco_acsmock/templates/layouts/default.html.ep [mkdir] /home/hendrik/net_cisco_acsmock/templates/example [write] /home/hendrik/net_cisco_acsmock/templates/example/welcome.html.ep
Create SQLite DB# sqlite3 acs.db
sqlite > create table users(id integer, description text, name text, identitygroupname text, changepassword text, enablepassword text, enabled integer, password text, passwordneverexpires integer, passwordtype text, dateexceeds text, dateexceedsenabled integer);sqlite > insert into users (id, description, name, identitygroupname, changepassword, enablepassword, enabled, password,
passwordneverexpires, passwordtype, dateexceeds, dateexceedsenabled)values(1, 'Description for #1', 'Foo', 'All Groups', 'Secret Change','Secret Enable',1,'Secret',1,'Internal','2020-01-01',1);sqlite > insert into users (id, description, name, identitygroupname, changepassword, enablepassword, enabled, password,
passwordneverexpires, passwordtype, dateexceeds, dateexceedsenabled)values(2, 'Description for #2', 'Bar', 'All Groups', 'Secret Change','Secret Enable',1,'Secret',1,'Internal','2020-01-01',1);
sqlite > .quit
Generate DBIx::Class SchemaGenerate DBIX::Class classes automatically based on SQLite database (or any other
database)
# dbicdump -o dump_directory=./lib \ -o components='["InflateColumn::DateTime"]' \ -o debug=1 \ Net::Cisco::ACS::Mock::Schema dbi:SQLite:acs.db
Run this as much as needed.. but it will overwrite the files!
Suggestion: Make sure to prepare your foreign keys properly!
lib/Net/Cisco/ACS/Mock.pm
package Net::Cisco::ACS::Mock;use Mojo::Base 'Mojolicious'; use Net::Cisco::ACS::Mock::Schema;
sub startup {my $self = shift; my $schema = Net::Cisco::ACS::Mock::Schema->connect('dbi:SQLite:acs.db'); $self->helper(db => sub { return $schema; });my $r = $self->routes;
$r->get("/Rest/Identity/User/name/:name")->to('User#query'); $r->get('/Rest/Identity/User/id/:id')->to('User#query'); $r->get('/Rest/Identity/User')->to('User#query'); $r->put('/Rest/Identity/User')->to('User#update'); $r->post('/Rest/Identity/User')->to('User#create'); $r->delete('/Rest/Identity/User/name/:name')->to('User#delete');$r->delete('/Rest/Identity/User/id/:id')->to('User#delete');
}1;
lib/Net/Cisco/ACS/Mock.pm
package Net::Cisco::ACS::Mock;use Mojo::Base 'Mojolicious'; use Net::Cisco::ACS::Mock::Schema;
sub startup {my $self = shift;
my $schema = Net::Cisco::ACS::Mock::Schema->connect('dbi:SQLite:acs.db');$self->helper(db => sub { return $schema; });my $r = $self->routes;
$r->get("/Rest/Identity/User/name/:name")->to('User#query'); $r->get('/Rest/Identity/User/id/:id')->to('User#query'); $r->get('/Rest/Identity/User')->to('User#query'); $r->put('/Rest/Identity/User')->to('User#update'); $r->post('/Rest/Identity/User')->to('User#create'); $r->delete('/Rest/Identity/User/name/:name')->to('User#delete');$r->delete('/Rest/Identity/User/id/:id')->to('User#delete');
}1;
Startup is called on Mojo startupStore DBIx::Class connection in $self->db
lib/Net/Cisco/ACS/Mock.pm
package Net::Cisco::ACS::Mock;use Mojo::Base 'Mojolicious'; use Net::Cisco::ACS::Mock::Schema;
sub startup {my $self = shift; my $schema = Net::Cisco::ACS::Mock::Schema->connect('dbi:SQLite:acs.db'); $self->helper(db => sub { return $schema; });my $r = $self->routes;
$r->get("/Rest/Identity/User/name/:name")->to('User#query'); $r->get('/Rest/Identity/User/id/:id')->to('User#query'); $r->get('/Rest/Identity/User')->to('User#query'); $r->put('/Rest/Identity/User')->to('User#update'); $r->post('/Rest/Identity/User')->to('User#create'); $r->delete('/Rest/Identity/User/name/:name')->to('User#delete');$r->delete('/Rest/Identity/User/id/:id')->to('User#delete');
}1;
lib/Net/Cisco/ACS/Mock.pm
package Net::Cisco::ACS::Mock;use Mojo::Base 'Mojolicious'; use Net::Cisco::ACS::Mock::Schema;
sub startup {my $self = shift; my $schema = Net::Cisco::ACS::Mock::Schema->connect('dbi:SQLite:acs.db'); $self->helper(db => sub { return $schema; });my $r = $self->routes;
$r->get("/Rest/Identity/User/name/:name")->to('User#query'); $r->get('/Rest/Identity/User/id/:id')->to('User#query'); $r->get('/Rest/Identity/User')->to('User#query'); $r->put('/Rest/Identity/User')->to('User#update'); $r->post('/Rest/Identity/User')->to('User#create'); $r->delete('/Rest/Identity/User/name/:name')->to('User#delete');$r->delete('/Rest/Identity/User/id/:id')->to('User#delete');
}1;
Send GET request to /Rest/Identity/User/name/*
to Net::Cisco::ACS::Mock::Controller::User
Method: queryArgument ‘name’ retrievable through
param
lib/Net/Cisco/ACS/Mock/Schema.pm
package Net::Cisco::ACS::Mock::Schema;
# based on the DBIx::Class Schema base classuse base qw/DBIx::Class::Schema/;
# This will load any classes within__PACKAGE__->load_namespaces();
1;
Generated by dbicdump
lib/Net/Cisco/ACS/Mock/Schema/Result/User.pm
package Net::Cisco::ACS::Mock::Schema::Result::User;use base qw/DBIx::Class::Core/;
# Associated table in database__PACKAGE__->table('users');
# Column definition__PACKAGE__->add_columns( id => { data_type => 'integer', is_auto_increment => 1 }, description => { data_type => 'text’ }, identitygroupname => { data_type => 'text‘ }, name => { data_type => 'text‘ }, changepassword => { data_type => 'text‘ }, enablepassword => { data_type => 'text‘ }, enabled => { data_type => 'integer’ },
lib/Net/Cisco/ACS/Mock/Schema/Result/User.pm
package Net::Cisco::ACS::Mock::Schema::Result::User;use base qw/DBIx::Class::Core/;
# Associated table in database__PACKAGE__->table('users');
# Column definition__PACKAGE__->add_columns( id => { data_type => 'integer', is_auto_increment => 1 }, description => { data_type => 'text’ }, identitygroupname => { data_type => 'text‘ }, name => { data_type => 'text‘ }, changepassword => { data_type => 'text‘ }, enablepassword => { data_type => 'text‘ }, enabled => { data_type => 'integer’ },
Generated by dbicdumpResult package
Connects to DB table Users
lib/Net/Cisco/ACS/Mock/Schema/Result/User.pm
..CONTINUED
password => { data_type => 'text‘ }, passwordneverexpires => { data_type => 'integer’ }, passwordtype => { data_type => 'text‘ }, dateexceeds => { data_type => 'text’ }, dateexceedsenabled => { data_type => 'integer’ },);
# Tell DBIC that 'id' is the primary key __PACKAGE__->set_primary_key('id');
1;
lib/Net/Cisco/ACS/Mock/Controller/User.pm
package Net::Cisco::ACS::Mock::Controller::User;use Mojo::Base 'Mojolicious::Controller';use XML::Simple;
sub query { my $self = shift; my $name = $self->param("name"); my $id = $self->param("id"); my $rs = $self->db->resultset('User'); my $user; if ($name) { my $query_rs = $rs->search({ name => $name }); $user = $query_rs->first; } if ($id) { my $query_rs = $rs->search({ id => $id }); $user = $query_rs->first; }
lib/Net/Cisco/ACS/Mock/Controller/User.pm
package Net::Cisco::ACS::Mock::Controller::User;use Mojo::Base 'Mojolicious::Controller';use XML::Simple;
sub query { my $self = shift; my $name = $self->param("name"); my $id = $self->param("id"); my $rs = $self->db->resultset('User'); my $user; if ($name) { my $query_rs = $rs->search({ name => $name }); $user = $query_rs->first; } if ($id) { my $query_rs = $rs->search({ id => $id }); $user = $query_rs->first; }
View Controller for /UserMethod query maps to #query earlier
lib/Net/Cisco/ACS/Mock/Controller/User.pm
package Net::Cisco::ACS::Mock::Controller::User;use Mojo::Base 'Mojolicious::Controller';use XML::Simple;
sub query { my $self = shift; my $name = $self->param("name"); my $id = $self->param("id"); my $rs = $self->db->resultset('User'); my $user; if ($name) { my $query_rs = $rs->search({ name => $name }); $user = $query_rs->first; } if ($id) { my $query_rs = $rs->search({ id => $id }); $user = $query_rs->first; }
Map to :id and :name in URI
lib/Net/Cisco/ACS/Mock/Controller/User.pm
package Net::Cisco::ACS::Mock::Controller::User;use Mojo::Base 'Mojolicious::Controller';use XML::Simple;
sub query { my $self = shift; my $name = $self->param("name"); my $id = $self->param("id"); my $rs = $self->db->resultset('User'); my $user; if ($name) { my $query_rs = $rs->search({ name => $name }); $user = $query_rs->first; } if ($id) { my $query_rs = $rs->search({ id => $id }); $user = $query_rs->first; }
Load Net::Cisco::ACS::Mock::Schema::Result::User
Query table with criteria
lib/Net/Cisco/ACS/Mock/Controller/User.pm
if (!$id && !$name) { my $query_rs = $rs->search; my %users = (); while (my $account = $query_rs->next) { $users{$account->name} = { # Set the record }; } } $self->stash("users" => \%users); $self->render(template => 'user/queryall', format => 'xml', layout => 'userall',status => 200); return; } $self->stash("user" => $user); $self->render(template => 'user/query', format => 'xml', layout => 'user', status => 200);}
lib/Net/Cisco/ACS/Mock/Controller/User.pm
if (!$id && !$name) { my $query_rs = $rs->search; my %users = (); while (my $account = $query_rs->next) { $users{$account->name} = { # Set the record }; } } $self->stash("users" => \%users); $self->render(template => 'user/queryall', format => 'xml', layout => 'userall'); return; } $self->stash("user" => $user); $self->render(template => 'user/query', format => 'xml', layout => 'user');}
Stash maps variables to template variables
lib/Net/Cisco/ACS/Mock/Controller/User.pm
if (!$id && !$name) { my $query_rs = $rs->search; my %users = (); while (my $account = $query_rs->next) { $users{$account->name} = { # Set the record }; } } $self->stash("users" => \%users); $self->render(template => 'user/queryall', format => 'xml', layout => 'userall', status => 200); return; } $self->stash("user" => $user); $self->render(template => 'user/query', format => 'xml', layout => 'user', status => 200);}
Render processes templates usingtemplate file,
format prepares HTTP header,layout wraps around the template
status returns HTTP status 200 (OK)
lib/Net/Cisco/ACS/Mock/Controller/User.pm
..CONTINUED
sub update { my $self = shift; my $rs = $self->db->resultset('User'); my $data = $self->req->body; my $xmlsimple = XML::Simple->new(); my $xmlout = $xmlsimple->XMLin($data); my $query_rs = $rs->search({ name => $xmlout->{"name"} }); my $account = $query_rs->first;
$account->update({ # Set Record });$self->render(template => 'user/userresult', format => 'xml', layout => 'userresult', status => 200);}
lib/Net/Cisco/ACS/Mock/Controller/User.pm
..CONTINUED
sub create { my $self = shift; my $data = $self->req->body; my $xmlsimple = XML::Simple->new(); my $xmlout = $xmlsimple->XMLin($data); my $rsmax = $self->db->resultset('User')->get_column('Id'); my $maxid = $rsmax->max; $maxid++;
$self->db->resultset('User')->create({ # Set record id => $maxid });$self->render(template => 'user/userresult', format => 'xml', layout => 'userresult', status => 200);
}
lib/Net/Cisco/ACS/Mock/Controller/User.pm
..CONTINUED
sub delete { my $self = shift; my $rs = $self->db->resultset('User'); my $name = $self->param("name"); my $id = $self->param("id"); my $user; if ($name) { my $query_rs = $rs->search({ name => $name }); $user = $query_rs->first; } if ($id) { my $query_rs = $rs->search({ id => $id }); $user = $query_rs->first; } $user->delete if $user; $self->render(template => 'user/userresult', format => 'xml', layout => 'userresult', status =>
200);}1;
templates/user/queryall.xml.ep
% foreach my $user (sort keys %{$users}) { <User><id><%= $users->{$user}->{id} %></id><description><%= $users->{$user}->{description} %></description><identityGroupName><%= $users->{$user}->{identitygroupname} %></identityGroupName><name><%= $users->{$user}->{name} %></name><changePassword><%= $users->{$user}->{changepassword} %></changePassword><enablePassword><%= $users->{$user}->{enablepassword} %></enablePassword><enabled><%= $users->{$user}->{enabled} %></enabled><password><%= $users->{$user}->{password} %></password><passwordNeverExpires><%= $users->{$user}->{passwordneverexpires}
%></passwordNeverExpires><passwordType><%= $users->{$user}->{passwordtype} %></passwordType><dateExceeds><%= $users->{$user}->{dateexceeds} %></dateExceeds><dateExceedsEnabled><%= $users->{$user}->{dateexceedsenabled} %></dateExceedsEnabled></User>% }
templates/user/queryall.xml.ep
% foreach my $user (sort keys %{$users}) { <User><id><%= $users->{$user}->{id} %></id><description><%= $users->{$user}->{description} %></description><identityGroupName><%= $users->{$user}->{identitygroupname} %></identityGroupName><name><%= $users->{$user}->{name} %></name><changePassword><%= $users->{$user}->{changepassword} %></changePassword><enablePassword><%= $users->{$user}->{enablepassword} %></enablePassword><enabled><%= $users->{$user}->{enabled} %></enabled><password><%= $users->{$user}->{password} %></password><passwordNeverExpires><%= $users->{$user}->{passwordneverexpires}
%></passwordNeverExpires><passwordType><%= $users->{$user}->{passwordtype} %></passwordType><dateExceeds><%= $users->{$user}->{dateexceeds} %></dateExceeds><dateExceedsEnabled><%= $users->{$user}->{dateexceedsenabled} %></dateExceedsEnabled></User>% }
Template can contained embedded Perl,in this case a for loop, generating elements
templates/layouts/userall.xml.ep
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ns2:users xmlns:ns2="identity.rest.mgmt.acs.nm.cisco.com">
<%= content %></ns2:users>
templates/layouts/userall.xml.ep
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ns2:users xmlns:ns2="identity.rest.mgmt.acs.nm.cisco.com">
<%= content %></ns2:users>
Layout wraps around template file.Content is placed in <%= content %>
templates/user/query.xml.ep
<id><%= $user->id %></id><description><%= $user->description %></description><identityGroupName><%= $user->identitygroupname %></identityGroupName><name><%= $user->name %></name><changePassword><%= $user->changepassword %></changePassword><enablePassword><%= $user->enablepassword %></enablePassword><enabled><%= $user->enabled %></enabled><password><%= $user->password %></password><passwordNeverExpires><%= $user->passwordneverexpires %></passwordNeverExpires><passwordType><%= $user->passwordtype %></passwordType><dateExceeds><%= $user->dateexceeds %></dateExceeds><dateExceedsEnabled><%= $user->dateexceedsenabled %></dateExceedsEnabled>
templates/layouts/user.xml.ep
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ns2:user xmlns:ns2="identity.rest.mgmt.acs.nm.cisco.com">
<%= content %></ns2:user>
Morbo Development Server Single request Built using Mojolicious Supports all features # morbo script/net_cisco_acsmock Server available at http://127.0.0.1:3000
Production Server Multiple requests, scalable Built using Mojolicious Supports all features # hypnotoad script/net_cisco_acsmock [Sun Dec 11 15:19:16 2016] [info] Listening at "http://*:8080"Server available at http://127.0.0.1:8080
HypnoToad
The proof of the pudding..#!/usr/bin/perl
use lib qw(Net/Cisco/ACS/lib);use Net::Cisco::ACS;use Data::Dumper;
my $acs = Net::Cisco::ACS->new(hostname => '127.0.0.1:8080', ssl=>0, username => 'acsadmin', password => 'password');
print Dumper $acs->users;
OutputNet::Cisco::ACS:
$VAR1 = { 'Foo' => bless( { 'id' => '1', 'passwordType' => 'Internal', 'name' => 'Foo', 'enablePassword' => 'Secret Enable', 'passwordNeverExpires' => '1', 'password' => 'Secret', 'description' => 'Description for #1', 'changePassword' => 'Secret Change', 'identityGroupName' => 'All Groups', 'dateExceedsEnabled' => '1', 'enabled' => '0', 'dateExceeds' => '2020-01-01' }, 'Net::Cisco::ACS::User' ),
'Bar' => bless( { 'dateExceedsEnabled' => '1', 'enabled' => '0', 'dateExceeds' => '2020-01-01', 'description' => 'Description for #2', 'passwordNeverExpires' => '1', 'password' => 'Secret', 'identityGroupName' => 'All Groups', 'changePassword' => 'Secret Change', 'enablePassword' => 'Secret Enable', 'name' => 'Bar', 'passwordType' => 'Internal', 'id' => '2' }, 'Net::Cisco::ACS::User' ) };
Outputhttp://localhost:3000/Rest/Identity/User/name/Foo
<?xml version="1.0" encoding="UTF-8" standalone="true"?><ns2:user xmlns:ns2="identity.rest.mgmt.acs.nm.cisco.com"><id>1</id><description>Description for #1</description><identityGroupName>All Groups</identityGroupName><name>Foo</name><changePassword>Secret Change</changePassword><enablePassword>Secret Enable</enablePassword><enabled>1</enabled><password>Secret</password><passwordNeverExpires>1</passwordNeverExpires><passwordType>Internal</passwordType><dateExceeds>2020-01-01</dateExceeds><dateExceedsEnabled>1</dateExceedsEnabled></ns2:user>