App-Netdisco

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

  * Run daemons as target binary's owning user (supports run control)
  * Clean up library path fiddling across all scripts
  * Rename housekeeping expiry task to be expire
  * Refactor nbtstat to group probes by device where node was arped
  * Add note about pgtune to install docs

  [BUG FIXES]

  * Suggest installation of perl-core RPM (arguably Red Hat's bug not ours!)
  * FATAL warnings considered harmful
  * Only show STP blocked icon if port is UP
  * Store device model and serial via UTF8 conversion
  * Only set housekeeping default if user has enabled

2.024004 - 2014-03-04

  [BUG FIXES]

  * Fix Login and Logout that were broken with SSL

2.024003_001 - 2014-03-03

lib/App/Netdisco/Backend/Role/Manager.pm  view on Meta::CPAN

package App::Netdisco::Backend::Role::Manager;

use Dancer qw/:moose :syntax :script/;

use List::Util 'sum';
use Proc::ProcessTable;
use App::Netdisco::Util::MCE;

use App::Netdisco::Backend::Job;
use App::Netdisco::JobQueue
  qw/jq_locked jq_getsome jq_lock jq_warm_thrusters/;

use Role::Tiny;
use namespace::clean;

sub worker_begin {
  my $self = shift;
  my $wid = $self->wid;

  return debug "mgr ($wid): no need for manager... skip begin"
    if setting('workers')->{'no_manager'};

lib/App/Netdisco/Backend/Role/Manager.pm  view on Meta::CPAN

  # job queue initialisation
  # the expensive parts of this were moved to primeskiplist job
  jq_warm_thrusters;

  # queue a job to rebuild the device action skip list
  $self->{queue}->enqueuep(200,
    App::Netdisco::Backend::Job->new({ job => 0, action => 'primeskiplist' }));

  # requeue jobs locally
  debug "mgr ($wid): searching for jobs booked to this processing node";
  my @jobs = jq_locked;

  if (scalar @jobs) {
      info sprintf "mgr (%s): found %s jobs booked to this processing node",
        $wid, scalar @jobs;
      $self->{queue}->enqueuep(100, @jobs);
  }
}

# creates a 'signature' for each job so that we can check for duplicates ...
# it happens from time to time due to the distributed nature of the job queue

lib/App/Netdisco/DB/ExplicitLocking.pm  view on Meta::CPAN

  );
}

use constant \%lock_modes;

use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = (keys %lock_modes);
our %EXPORT_TAGS = (modes => \@EXPORT_OK);

sub txn_do_locked {
  my ($self, $table, $mode, $sub) = @_;
  my $sql_fmt = q{LOCK TABLE %s IN %%s MODE};
  my $schema = $self;

  if ($self->can('result_source')) {
      # ResultSet component
      $sub = $mode;
      $mode = $table;
      $table = $self->result_source->from;
      $schema = $self->result_source->schema;
  }

  $schema->throw_exception('missing Table name to txn_do_locked()')
    unless $table;

  $table = [$table] if ref '' eq ref $table;
  my $table_fmt = join ', ', ('%s' x scalar @$table);
  my $sql = sprintf $sql_fmt, $table_fmt;

  if (ref '' eq ref $mode and $mode) {
      scalar grep {$_ eq $mode} values %lock_modes
        or $schema->throw_exception('bad LOCK_MODE to txn_do_locked()');
  }
  else {
      $sub = $mode;
      $mode = 'ACCESS EXCLUSIVE';
  }

  $schema->txn_do(sub {
      my @params = map {$schema->storage->dbh->quote_identifier($_)} @$table;
      $schema->storage->dbh->do(sprintf $sql, @params, $mode);
      $sub->();

lib/App/Netdisco/DB/ExplicitLocking.pm  view on Meta::CPAN

=head1 SYNOPSIS

In your L<DBIx::Class> schema:

 package My::Schema;
 __PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');

Then, in your application code:

 use App::Netdisco::DB::ExplicitLocking ':modes';
 $schema->txn_do_locked($table, MODE_NAME, sub { ... });

This also works for the ResultSet:

 package My::Schema::ResultSet::TableName;
 __PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');

Then, in your application code:

 use App::Netdisco::DB::ExplicitLocking ':modes';
 $schema->resultset('TableName')->txn_do_locked(MODE_NAME, sub { ... });

=head1 DESCRIPTION

This L<DBIx::Class> component provides an easy way to execute PostgreSQL table
locks before a transaction block.

You can load the component in either the Schema class or ResultSet class (or
both) and then use an interface very similar to C<DBIx::Class>'s C<txn_do()>.

The package also exports constants for each of the table lock modes supported

lib/App/Netdisco/DB/ExplicitLocking.pm  view on Meta::CPAN

=item * C<SHARE_ROW_EXCLUSIVE>

=item * C<EXCLUSIVE>

=item * C<ACCESS_EXCLUSIVE>

=back

=head1 METHODS

=head2 C<< $schema->txn_do_locked($table|\@tables, MODE_NAME?, $subref) >>

This is the method signature used when the component is loaded into your
Schema class. The reason you might want to use this over the ResultSet version
(below) is to specify multiple tables to be locked before the transaction.

The first argument is one or more tables, and is required. Note that these are
the real table names in PostgreSQL, and not C<DBIx::Class> ResultSet aliases
or anything like that.

The mode name is optional, and defaults to C<ACCESS EXCLUSIVE>. You must use
one of the exported constants in this parameter.

Finally pass a subroutine reference, just as you would to the normal
C<DBIx::Class> C<txn_do()> method. Note that additional arguments are not
supported.

=head2 C<< $resultset->txn_do_locked(MODE_NAME?, $subref) >>

This is the method signature used when the component is loaded into your
ResultSet class. If you don't yet have a ResultSet class (which is the default
- normally only Result classes are created) then you can create a stub which
simply loads this component (and inherits from C<DBIx::Class::ResultSet>).

This is the simplest way to use this module if you only want to lock one table
before your transaction block.

The first argument is the optional mode name, which defaults to C<ACCESS

lib/App/Netdisco/JobQueue.pm  view on Meta::CPAN


use Module::Load ();
Module::Load::load
  'App::Netdisco::JobQueue::' . $ENV{ND_QUEUE_ENGINE} => ':all';

use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/
  jq_warm_thrusters
  jq_getsome
  jq_locked
  jq_queued
  jq_lock
  jq_defer
  jq_complete
  jq_log
  jq_userlog
  jq_insert
  jq_delete
/;
our %EXPORT_TAGS = ( all => \@EXPORT_OK );

lib/App/Netdisco/JobQueue.pm  view on Meta::CPAN

subroutines.

=head1 EXPORT_OK

=head2 jq_getsome( $num? )

Returns a list of randomly selected queued jobs. Default is to return one job,
unless C<$num> is provided. Jobs are returned as objects which implement the
Netdisco job instance interface (see below).

=head2 jq_locked()

Returns the list of jobs currently booked out to this processing node (denoted
by the local hostname). Jobs are returned as objects which implement the
Netdisco job instance interface (see below).

=head2 jq_queued( $job_type )

Returns a list of IP addresses of devices which currently have a job of the
given C<$job_type> queued (e.g. C<discover>, C<arpnip>, etc).

lib/App/Netdisco/JobQueue/PostgreSQL.pm  view on Meta::CPAN

use App::Netdisco::DB::ExplicitLocking ':modes';

use JSON::PP ();
use Try::Tiny;

use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/
  jq_warm_thrusters
  jq_getsome
  jq_locked
  jq_queued
  jq_lock
  jq_defer
  jq_complete
  jq_log
  jq_userlog
  jq_insert
  jq_delete
/;
our %EXPORT_TAGS = ( all => \@EXPORT_OK );

lib/App/Netdisco/JobQueue/PostgreSQL.pm  view on Meta::CPAN

  while (my $job = $tasty->next) {
    if ($job->device
      and not scalar grep {$job->action eq $_} @{ setting('job_targets_prefix') }) {

      # need to handle device discovered since backend daemon started
      # and the skiplist was primed. these should be checked against
      # the various acls and have device_skip entry added if needed,
      # and return false if it should have been skipped.
      my @badactions = get_denied_actions($job->device);
      if (scalar @badactions) {
        schema(vars->{'tenant'})->resultset('DeviceSkip')->txn_do_locked(EXCLUSIVE, sub {
            schema(vars->{'tenant'})->resultset('DeviceSkip')->find_or_create({
              backend => setting('workers')->{'BACKEND'}, device => $job->device,
            },{ key => 'device_skip_pkey' })->add_to_actionset(@badactions);
        });

        # will now not be selected in a future _getsome()
        next if scalar grep {$_ eq $job->action} @badactions;
      }
    }

lib/App/Netdisco/JobQueue/PostgreSQL.pm  view on Meta::CPAN

    }, { for => 'update' })
        ->update({ status => 'info', log => (sprintf 'duplicate of %s', $job->id) });

    debug sprintf 'getsome: cancelled %s duplicate(s) of job %s', ($gone || 0), $job->id;
    push @returned, App::Netdisco::Backend::Job->new({ $job->get_columns });
  }

  return @returned;
}

sub jq_locked {
  my @returned = ();
  my $rs = schema(vars->{'tenant'})->resultset('Admin')->search({
    status  => 'queued',
    backend => setting('workers')->{'BACKEND'},
    started => \[q/> (LOCALTIMESTAMP - ?::interval)/, setting('jobs_stale_after')],
  });

  while (my $job = $rs->next) {
      push @returned, App::Netdisco::Backend::Job->new({ $job->get_columns });
  }

lib/App/Netdisco/JobQueue/PostgreSQL.pm  view on Meta::CPAN

  # arpnip (and every other action) will be prevented on the device.

  # seeing as defer is only triggered by an SNMP connect failure, this
  # behaviour seems reasonable, to me (or desirable, perhaps).

  # the deferrable_actions setting exists as a workaround to this behaviour
  # should it be needed by any action (that is, per-device action but
  # do not increment deferrals count and simply try to run again).

  try {
    schema(vars->{'tenant'})->resultset('DeviceSkip')->txn_do_locked(EXCLUSIVE, sub {
      if ($job->device
          and not scalar grep { $job->action eq $_ }
                              @{ setting('deferrable_actions') || [] }) {

        schema(vars->{'tenant'})->resultset('DeviceSkip')->find_or_create({
          backend => setting('workers')->{'BACKEND'}, device => $job->device,
        },{ key => 'device_skip_pkey' })->increment_deferrals;
      }

      debug sprintf 'defer: job %s', ($job->id || 'unknown');

lib/App/Netdisco/JobQueue/PostgreSQL.pm  view on Meta::CPAN

  my $job = shift;
  my $happy = false;

  # lock db row and update to show job is done/error

  # now that SNMP connect failures are deferrals and not errors, any complete
  # status, whether success or failure, indicates an SNMP connect. reset the
  # connection failures counter to forget about occasional connect glitches.

  try {
    schema(vars->{'tenant'})->resultset('DeviceSkip')->txn_do_locked(EXCLUSIVE, sub {
      if ($job->device and not $job->is_offline
            and not scalar grep {$job->action eq $_} @{ setting('job_targets_prefix') }) {

        schema(vars->{'tenant'})->resultset('DeviceSkip')->find_or_create({
          backend => setting('workers')->{'BACKEND'}, device => $job->device,
        },{ key => 'device_skip_pkey' })->update({ deferrals => 0 });
      }

      schema(vars->{'tenant'})->resultset('Admin')
        ->search({ job => $job->id }, { for => 'update' })

lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm  view on Meta::CPAN

ajax '/ajax/content/admin/jobqueue' => require_role admin => sub {
    content_type('text/html');

    my @backends = schema(vars->{'tenant'})->resultset('DeviceSkip')
        ->search({device => '255.255.255.255'})->hri->all;

    my $num_backends = scalar @backends;
    my $tot_workers  = 0;
    $tot_workers += $_->{deferrals} for @backends;

    my $jq_locked = schema(vars->{'tenant'})->resultset('Admin')
      ->search({status => 'queued', backend => { '!=' => undef }})->count();

    my $jq_backlog = schema(vars->{'tenant'})->resultset('Admin')
      ->search({status => 'queued', backend => undef })->count();

    my $jq_done = schema(vars->{'tenant'})->resultset('Admin')
      ->search({status => 'done'})->count();

    my $jq_errored = schema(vars->{'tenant'})->resultset('Admin')
      ->search({status => 'error'})->count();

lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm  view on Meta::CPAN

        backend => { '!=' => undef },
        started => \[q/<= (LOCALTIMESTAMP - ?::interval)/, setting('jobs_stale_after')],
    })->count();

    my $jq_total = schema(vars->{'tenant'})->resultset('Admin')->count();

    template 'ajax/admintask/jobqueue.tt', {
      num_backends => commify($num_backends || '?'),
      tot_workers  => commify($tot_workers || '?'),

      jq_running => commify($jq_locked - $jq_stale),
      jq_backlog => commify($jq_backlog),
      jq_done => commify($jq_done),
      jq_errored => commify($jq_errored),
      jq_stale => commify($jq_stale),
      jq_total => commify($jq_total),

      results => [ jq_log ],
    }, { layout => undef };
};

lib/App/Netdisco/Web/Plugin/AdminTask/TimedOutDevices.pm  view on Meta::CPAN


register_admin_task({
  tag => 'timedoutdevices',
  label => 'SNMP Connect Failures',
});

ajax '/ajax/control/admin/timedoutdevices/del' => require_role admin => sub {
    send_error('Missing backend', 400) unless param('backend');
    send_error('Missing device',  400) unless param('device');

    schema(vars->{'tenant'})->resultset('DeviceSkip')->txn_do_locked(EXCLUSIVE, sub {
      schema(vars->{'tenant'})->resultset('DeviceSkip')->find_or_create({
        backend => param('backend'), device => param('device'),
      },{ key => 'device_skip_pkey' })->update({ deferrals => 0 });
    });

    return '';
};

ajax '/ajax/content/admin/timedoutdevices' => require_role admin => sub {
    my @set = schema(vars->{'tenant'})->resultset('DeviceSkip')->search({

lib/App/Netdisco/Web/Plugin/AdminTask/UndiscoveredNeighbors.pm  view on Meta::CPAN

use App::Netdisco::Web::Plugin;

register_admin_task(
    {   tag          => 'undiscoveredneighbors',
        label        => 'Undiscovered Neighbors',
        provides_csv => 1,
    }
);

# just to note a problem with this query:
# using DeviceSkip to see if discover is blocked, but that table only shows
# blocked actions on backends not permitted, so there may be a backend running
# that permits the action, we would not know.

get '/ajax/content/admin/undiscoveredneighbors' => require_role admin => sub {
    my @results
        = schema(vars->{'tenant'})->resultset('Virtual::UndiscoveredNeighbors')->hri->all;
    return unless scalar @results;

    if ( request->is_ajax ) {
        template 'ajax/admintask/undiscoveredneighbors.tt',
            { results => \@results, },

lib/App/Netdisco/Web/Plugin/Report/PortBlocking.pm  view on Meta::CPAN


use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;

use App::Netdisco::Web::Plugin;

register_report(
    {   category     => 'Port',
        tag          => 'portblocking',
        label        => 'Blocked - Spanning Tree',
        provides_csv => 1,
        api_endpoint => 1,
    }
);

get '/ajax/content/report/portblocking' => require_login sub {
    my @results = schema(vars->{'tenant'})->resultset('Device')->search(
        { 'stp' => [ 'blocking', 'broken' ], 'up' => { '!=', 'down' } },
        {   select       => [ 'ip', 'dns', 'name' ],
            join         => ['ports'],

lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm  view on Meta::CPAN

                  $device->ip, $port, $field, $oldval, $newval;
                last PORTDIFF;
            }
        }
    }
  }

  # support for Hooks
  vars->{'hook_data'}->{'ports'} = [values %deviceports];

  schema('netdisco')->resultset('DevicePort')->txn_do_locked(ACCESS_EXCLUSIVE, sub {
    my $coder = JSON::PP->new->utf8(0)->allow_nonref(1)->allow_unknown(1);

    # backup the custom_fields
    my %fields = map  {($_->port => $coder->decode(Encode::encode('UTF-8',$_->custom_fields || '{}')))}
                 grep {exists $deviceports{$_->port}}
                      values %{ vars->{'device_ports'} };

    my %ok_fields = map {$_ => 1}
                    grep {defined}
                    map {$_->{name}}

lib/App/Netdisco/Worker/Plugin/Expire.pm  view on Meta::CPAN

  if (setting('expire_nodeip_orphans')) {
      schema('netdisco')->resultset('NodeIp')->search({
        mac => { -in => schema('netdisco')->resultset('NodeIp')->search(
          { port => undef },
          { join => 'nodes', select => [{ distinct => 'me.mac' }], }
        )->as_query },
      })->delete;
  }

  if (setting('expire_jobs') and setting('expire_jobs') > 0) {
      schema('netdisco')->txn_do_locked('admin', EXCLUSIVE, sub {
        schema('netdisco')->resultset('Admin')->search({
          entered => \[q/< (LOCALTIMESTAMP - ?::interval)/,
              (setting('expire_jobs') * 86400)],
        })->delete();
      });
  }

  if (setting('expire_userlog') and setting('expire_userlog') > 0) {
      schema('netdisco')->txn_do_locked('admin', EXCLUSIVE, sub {
        schema('netdisco')->resultset('UserLog')->search({
          creation => \[q/< (LOCALTIMESTAMP - ?::interval)/,
              (setting('expire_userlog') * 86400)],
        })->delete();
      });
  }

  # now update stats
  update_stats();

share/config.yml  view on Meta::CPAN

        vendor,
        os,
        version
      FROM
        ranked_devices
      WHERE
        rn = 1
      ORDER BY
        device_name, serial
  - tag: portserrordisabled
    label: 'Blocked - Error-Disabled'
    category: Port
    columns:
      - { ip: Device, _searchable: true }
      - { dns: DNS }
      - { port: Port }
      - { name: Description }
      - { reason: Reason }
    query: |
      SELECT dp.ip, d.dns, dp.port, dp.name, properties.error_disable_cause AS reason
        FROM device_port dp

share/contrib/raddb/dictionary.shiva  view on Meta::CPAN

VALUE	Shiva-Disconnect-Reason Remote			1
VALUE	Shiva-Disconnect-Reason Error			2
VALUE	Shiva-Disconnect-Reason Idle-Timeout		3
VALUE	Shiva-Disconnect-Reason Session-Timeout		4
VALUE	Shiva-Disconnect-Reason Admin-Disconnect	5
VALUE	Shiva-Disconnect-Reason Dialback		6
VALUE	Shiva-Disconnect-Reason Virtual-Connection	7
VALUE	Shiva-Disconnect-Reason Bandwidth-On-Demand	8
VALUE	Shiva-Disconnect-Reason Failed-Authentication	9
VALUE	Shiva-Disconnect-Reason Preempted		10
VALUE	Shiva-Disconnect-Reason Blocked			11
VALUE	Shiva-Disconnect-Reason Tariff-Management	12
VALUE	Shiva-Disconnect-Reason Backup			13

#	Shiva Function Values

VALUE	Shiva-Function		Unknown			0
VALUE	Shiva-Function		Dialin			1
VALUE	Shiva-Function		Dialout			2
VALUE	Shiva-Function		Lan-To-Lan		3

share/contrib/raddb/dictionary.usr  view on Meta::CPAN

VALUE	USR-Call-Event-Code			noFreeIGW	      10
VALUE	USR-Call-Event-Code			igwRejectCall	      11
VALUE	USR-Call-Event-Code			igwSetupTimeout	      12
VALUE	USR-Call-Event-Code			noFreeTdmts	      13
VALUE	USR-Call-Event-Code			bcReject	      14
VALUE	USR-Call-Event-Code			ieReject	      15
VALUE	USR-Call-Event-Code			chidReject	      16
VALUE	USR-Call-Event-Code			progReject	      17
VALUE	USR-Call-Event-Code			callingPartyReject    18
VALUE	USR-Call-Event-Code			calledPartyReject     19
VALUE	USR-Call-Event-Code			blocked		      20
VALUE	USR-Call-Event-Code			analogBlocked	      21
VALUE	USR-Call-Event-Code			digitalBlocked	      22
VALUE	USR-Call-Event-Code			outOfService	      23
VALUE	USR-Call-Event-Code			busy		      24
VALUE	USR-Call-Event-Code			congestion	      25
VALUE	USR-Call-Event-Code			protocolError	      26 
VALUE	USR-Call-Event-Code			noFreeBchannel	      27
VALUE	USR-Call-Event-Code			inOutCallCollision    28
VALUE	USR-Call-Event-Code			inCallArrival		29
VALUE	USR-Call-Event-Code			outCallArrival		30
VALUE	USR-Call-Event-Code			inCallConnect		31
VALUE	USR-Call-Event-Code			outCallConnect		32

share/public/swagger-ui/swagger-ui-bundle.js  view on Meta::CPAN

/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(...

share/public/swagger-ui/swagger-ui-bundle.js.map  view on Meta::CPAN

{"version":3,"file":"swagger-ui-bundle.js","mappings":";CAAA,SAAUA,iCAAiCC,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAyB,gBAAID,IAE7BD,EAAsB,gBAAIC,GAC3B,CATD,CASGK,MAAM,cCRLC,EA...

share/public/swagger-ui/swagger-ui.css  view on Meta::CPAN

.swagger-ui{color:#3b4151;container-name:swagger-ui;container-type:inline-size;/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */font-family:sans-serif}.swagger-ui html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text...

/*# sourceMappingURL=swagger-ui.css.map*/

share/public/swagger-ui/swagger-ui.css.map  view on Meta::CPAN

{"version":3,"file":"swagger-ui.css","mappings":"AAAA,YCII,aCcU,CFfd,yBACE,2BACA,4EGEF,CFJE,sBEIF,CDWc,iCCCZ,0BACA,8BACA,kBAUF,QACE,iHAOF,aAME,gBAQF,aACE,eACA,4DAWF,aAGE,oBAOF,eACE,gBAQF,sBACE,SACA,iBACA,iBAQF,+BACE,cACA,eAWF,4BACE,qCACA,yBAQF,kBACE,...

xt/21-check_acl_no_ipaddr_only.t  view on Meta::CPAN

  use_ok( 'App::Netdisco::Util::DNS', 'hostname_from_ip' );
}

use Dancer qw/:script !pass/;

# this is needed so that test works in github action container
# and actually ends up cutting out live DNS anyway 👍
config->{'dns'} = { 'ETCHOSTS' => { localhost => [ [ '127.0.0.1' ] ] } };

config->{'dns'}->{'no'} = ['::1','fe80::/10','127.0.0.0/8','169.254.0.0/16'];
is(hostname_from_ip('127.0.0.1'), undef, '127.0.0.1 blocked');

config->{'dns'}->{'no'} = ['::1','fe80::/10','169.254.0.0/16'];
is(hostname_from_ip('127.0.0.1'), 'localhost', '127.0.0.1 allowed');

done_testing;

xt/32-swagger-ui-guard.t  view on Meta::CPAN


# This file asserts the CONTENT of the shipped Swagger UI page, not its
# behaviour, and the distinction matters when reading a pass.
#
# The failure it guards is a re-vendor: dropping in a fresh upstream index.html
# reverts every local change below at once and silently, which is exactly how the
# petstore default arrived in the first place. A content assertion catches that.
#
# It cannot prove the page is safe, because it shares its input with the code it
# checks. The behavioural evidence is a browser driven against the page with
# off-host requests blocked and recorded, and it does not live in this suite,
# because the distribution has no working JavaScript test path.

my $page = catfile( $FindBin::Bin, updir(),
    'share', 'public', 'swagger-ui', 'index.html' );

my $html = do {
    open my $fh, '<', $page or die "cannot read $page: $!";
    local $/;
    <$fh>;
};

xt/51-updated-device-hook.t  view on Meta::CPAN

package FakePortsRS; # stands in for a DBIC ResultSet of DevicePort rows
sub new { my ($class, @rows) = @_; return bless { rows => [@rows] }, $class; }
sub reset { return $_[0] }
sub all { return @{ $_[0]->{rows} } }
sub delete { return 0 }
sub populate { return 1 }

package FakeDevicePortRS; # only result_source (metadata) and locking are used
sub new { return bless {}, shift }
sub result_source { return App::Netdisco::DB->resultset('DevicePort')->result_source }
sub txn_do_locked { my ($self, $mode, $code) = @_; return $code->() }

package FakeSchema;
sub txn_do { my ($self, $code) = @_; return $code->() }
sub resultset {
  my ($self, $name) = @_;
  return FakeDevicePortRS->new if $name eq 'DevicePort';
  return App::Netdisco::DB->resultset($name);
}

package main;



( run in 1.811 second using v1.01-cache-2.11-cpan-800906f7e73 )