File Coverage

lib/Mojolicious/Plugin/Ubic.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Ubic;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::Ubic - Remote ubic admin
6              
7             =head1 SYNOPSIS
8              
9             #!perl
10             use Mojolicious::Lite;
11              
12             plugin Ubic => {
13             data_dir => '/path/to/ubic/data',
14             default_user => 'ubicadmin',
15             layout => 'my_layout',
16             remote_servers => [...],
17             route => app->routes->route('/something/secure'),
18             service_dir => '/path/to/ubic/service',
19             valid_actions => [...],
20             json => {
21             some => 'default values',
22             },
23             };
24              
25             app->start;
26              
27             See L for config description.
28              
29             =head1 DESCRIPTION
30              
31             This L plugin allow you to query status of the running L
32             services and also start/stop/restart/reload/... them.
33              
34             This is L on steroids.
35              
36             =cut
37              
38 3     3   5540 use Mojo::Base 'Mojolicious::Plugin';
  3         5  
  3         20  
39 3     3   11507 use Sys::Hostname;
  3         2808  
  3         190  
40 3     3   1442 use Ubic;
  0            
  0            
41             use Ubic::Settings;
42             use constant DEBUG => $ENV{UBIC_DEBUG} || 0;
43              
44             =head1 ACTIONS
45              
46             =head2 command
47              
48             POST /service/:service_name/start
49             POST /service/:service_name/reload
50             POST /service/:service_name/restart
51             POST /service/:service_name/stop
52              
53             Used to control a given service. The actions act like from the command
54             line. The return value contain "status". Example:
55              
56             {"status":"running"}
57              
58             =cut
59              
60             sub command {
61             my($self, $c) = @_;
62             my $command = $c->stash('command');
63             my $name = $c->stash('name');
64             my $json = $self->_json;
65             my $valid = grep { $command eq $_ } @{ $self->{valid_actions} };
66              
67             if(!$valid) {
68             $json->{error} = 'Invalid command';
69             return $c->render(json => $json, status => 400);
70             }
71             if(!Ubic->has_service($name)) {
72             $json->{error} = 'Not found';
73             return $c->render(json => $json, status => 404);
74             }
75              
76             if(Ubic->service($name)->isa('Ubic::Multiservice')) {
77             $json->{error} = 'Cannot run actions on multiservice';
78             return $c->render(json => $json, status => 400);
79             }
80              
81             eval {
82             $json->{status} = '' .Ubic->$command($name);
83             1;
84             } or do {
85             $json->{error} = $@ || 'Unknown error';
86             };
87              
88             $c->render(json => $json, status => $json->{error} ? 500 : 200);
89             }
90              
91             =head2 index
92              
93             GET /
94              
95             Draw a table of services using HTML.
96              
97             =cut
98              
99             sub index {
100             my($self, $c) = @_;
101             my $ua = $c->ua;
102              
103             $c->stash(layout => $self->{layout})->render_later;
104              
105             Mojo::IOLoop->delay(
106             sub {
107             my($delay) = @_;
108             for($self->_remote_servers($c)) {
109             my $url = $_->clone;
110             $url->query->param(flat => 1);
111             push @{ $url->path }, 'services';
112             warn "[UBIC] remote_url=$url\n" if DEBUG;
113             $ua->get($url, $delay->begin);
114             }
115             },
116             sub {
117             my($delay, @tx) = @_;
118             my @remotes;
119              
120             for my $tx (@tx) {
121             if(my $json = $tx->res->json) {
122             push @remotes, $json;
123             }
124             else {
125             push @remotes, { error => $tx->res->code || 'Did not respond' };
126             }
127              
128             $remotes[-1]{tx} = $tx;
129             }
130              
131             $c->render(template => 'ubic/index', remotes => \@remotes);
132             },
133             );
134             }
135              
136             =head2 proxy
137              
138             GET /proxy/#to/#service_name/:command
139              
140             This resource is used to proxy commands to other servers.
141              
142             =cut
143              
144             sub proxy {
145             my($self, $c) = @_;
146             my $to = $c->stash('to');
147             my $url;
148              
149             for($self->_remote_servers($c)) {
150             next unless $_->host eq $to;
151             $url = $_->clone;
152             push @{ $url->path }, 'service', $c->stash('name'), $c->stash('command');
153             last;
154             }
155              
156             unless($url) {
157             return $c->render(json => { error => 'Unknown host' }, status => 400);
158             }
159              
160             warn "[UBIC] remote_url=$url\n" if DEBUG;
161              
162             $c->render_later->ua->get($url => sub {
163             my($ua, $tx) = @_;
164             $c->render(
165             json => $tx->res->json || {},
166             status => $tx->res->code || 500,
167             );
168             });
169             }
170              
171             =head2 services
172              
173             GET /services
174             GET /services/:service_name
175              
176             Returns a json object with the services available and statuses:
177              
178             {
179             "multi_service_name": {
180             "child_service_name": {
181             "status":"running"
182             }
183             }
184             }
185              
186             Is is also possible to ask for "?flat=1" which will result in this response:
187              
188             {
189             "services": {
190             "multi_service_name.child_service_name": {
191             "status":"running"
192             }
193             }
194             }
195              
196             =cut
197              
198             sub services {
199             my($self, $c) = @_;
200             my $flat = $c->param('flat') ? $self->_json : undef;
201             my $json = $self->_json;
202             my $status_method = $c->param('cached') ? 'cached_status': 'status';
203             my $service;
204              
205             if(my $name = $c->stash('name')) {
206             if(!Ubic->has_service($name)) {
207             $json->{error} = 'Not found';
208             return $c->render(json => $json, status => 404);
209             }
210             $service = Ubic->service($name);
211             }
212             else {
213             $service = Ubic->root_service;
214             }
215              
216             $self->_traverse($service, $json, sub {
217             my($service, $data) = @_;
218              
219             unless($service->isa('Ubic::Multiservice')) {
220             $data->{status} = Ubic->$status_method($service->full_name);
221             $flat->{services}{$service->full_name}{status} = $data->{status} if $flat;
222             }
223             });
224              
225             $c->render(json => $flat ? $flat : $json);
226             }
227              
228             =head2 status
229              
230             GET /service/:service_name
231             GET /service/:service_name/status
232              
233             Used to get the status of a given service. Example JSON response:
234              
235             {"status":"running"}
236              
237             =cut
238              
239             sub status {
240             my($self, $c) = @_;
241             my $name = $c->stash('name');
242             my $json = $self->_json;
243             my $status_method = $c->param('cached') ? 'cached_status': 'status';
244              
245             if(!Ubic->has_service($name)) {
246             $json->{error} = 'Not found';
247             return $c->render(json => $json, status => 404);
248             }
249              
250             eval {
251             $json->{status} = Ubic->$status_method($name);
252             1;
253             } or do {
254             $json->{error} = $@;
255             };
256              
257             $c->render(json => $json, status => $json->{error} ? 500 : 200);
258             }
259              
260             =head1 METHODS
261              
262             =head2 register
263              
264             $app->plugin(Ubic => \%config);
265              
266             Will register the L above. Possible C<%config>:
267              
268             =over 4
269              
270             =item * data_dir
271              
272             Default to L.
273              
274             =item * default_user
275              
276             Default to L.
277              
278             =item * service_dir
279              
280             Default to L.
281              
282             =item * json
283              
284             A datastructure (hash-ref) which is included in all the responses. Could
285             contain data such as uptime, hostname, ...
286              
287             =item * layout
288              
289             Used to set the layout which the L will rendered inside.
290             Default is "ubic" which is defined in this package.
291              
292             =item * remote_servers
293              
294             A list of URL which point to other web servers compatible with the API
295             defined in this package. This allow L to run commands on all
296             servers, including the current. Example:
297              
298             [
299             "http://10.1.2.3/secret/ubic/path",
300             "http://10.1.2.4/other/secret/path",
301             ]
302              
303             =item * route
304              
305             A L object where the L should be mounted.
306              
307             =item * command_route
308              
309             A L object where L should be mounted. Default
310             is same as L.
311              
312             =item * valid_actions
313              
314             A list of valid actions for L to run. Default is:
315              
316             [ "start", "stop", "reload", "restart" ]
317              
318             =back
319              
320             =cut
321              
322             sub register {
323             my($self, $app, $config) = @_;
324             my $r = $config->{route} or die "'route' is required in config";
325             my $p = $config->{command_route} || $r;
326              
327             Ubic::Settings->data_dir($config->{data_dir}) if $config->{data_dir};
328             Ubic::Settings->default_user($config->{default_user}) if $config->{default_user};
329             Ubic::Settings->service_dir($config->{service_dir}) if $config->{service_dir};
330             Ubic::Settings->check_settings;
331              
332             $self->{json} = $config->{json} || {};
333             $self->{layout} = $config->{layout} || 'ubic';
334             $self->{remote_servers} = $config->{remote_servers} || [];
335             $self->{valid_actions} = $config->{valid_actions} || [qw( start stop reload restart )];
336              
337             for my $server (@{ $self->{remote_servers} }) {
338             next if ref $server;
339             $server = Mojo::URL->new($server);
340             }
341              
342             $r->get('/')->name('ubic_index')->to(cb => sub { $self->index(@_) });
343             $r->get('/services/*name', { name => '' })->name('ubic_services')->to(cb => sub { $self->services(@_) });
344             $r->get('/service/#name/:command', { command => 'status' }, [ command => 'status' ])->to(cb => sub { $self->status(@_) });
345             $p->any('/service/#name/:command')->name('ubic_service')->to(cb => sub { $self->command(@_) });
346             $p->any('/proxy/#to/#name/:command')->name('ubic_proxy')->to(cb => sub { $self->proxy(@_) });
347              
348             push @{ $app->renderer->classes }, __PACKAGE__;
349             }
350              
351             sub _json {
352             return { %{ shift->{json} } };
353             }
354              
355             sub _remote_servers {
356             my($self, $c) = @_;
357             my $servers = $self->{remote_servers};
358              
359             if(!$self->{init_remote_servers}++) {
360             push @$servers, $c->req->url->to_abs->clone;
361             }
362              
363             return @$servers;
364             }
365              
366             sub _traverse {
367             my($self, $service, $json, $cb) = @_;
368             my $name = $service->name;
369              
370             if($service->isa('Ubic::Multiservice')) {
371             my $name = $service->name;
372             my $data = $name ? $json->{$name} ||= {} : $json;
373              
374             $data->{services} ||= {};
375             $cb->($service, $data);
376             $self->_traverse($_, $data->{services}, $cb) for $service->services;
377             }
378             else {
379             $cb->($service, $json->{$name} ||= {});
380             }
381             }
382              
383             =head1 COPYRIGHT
384              
385             This is free software; you can redistribute it and/or modify it under the
386             same terms as the Perl 5 programming language system itself.
387              
388             =head1 AUTHOR
389              
390             Jan Henning Thorsen - C
391              
392             =cut
393              
394             1;
395              
396             __DATA__