parallel fetching of multiple hosts
[scpubgit/System-Introspector.git] / lib / System / Introspector / State.pm
CommitLineData
60e1cc39 1package System::Introspector::State;
2use Moo;
b079a95d 3use File::Tree::Snapshot;
60e1cc39 4use System::Introspector::Gatherer;
949dba9c 5use Object::Remote::Future qw( await_all );
60e1cc39 6
0a11cf83 7use JSON::Diffable qw( encode_json );
8
60e1cc39 9has config => (is => 'ro', required => 1);
10
b079a95d 11has root => (is => 'ro', required => 1);
60e1cc39 12
b079a95d 13sub user { $_[0]->config->user }
60e1cc39 14
b079a95d 15sub sudo_user { $_[0]->config->sudo_user }
a5e1e1c6 16
949dba9c 17sub _log { shift; printf "[%s] %s\n", scalar(localtime), join '', @_ }
18
b079a95d 19sub gather {
20 my ($self, @groups) = @_;
949dba9c 21 $self->_log('Start');
22 for my $group (@groups) {
23 my @waiting;
24 for my $host ($self->config->hosts) {
25 $self->_log("Beginning to fetch group '$group' on '$host'");
26 push @waiting, [$host, $self->fetch($host, $group)];
27 }
28 $self->_log("Now waiting for results");
29 my @results = map {
30 my ($host, @futures) = @$_;
31 my $done = [$host, await_all @futures];
32 $self->_log("Received all from group '$group' on '$host'");
33 $done;
34 } @waiting;
35 $self->_log("All gathered for group '$group'");
36 for my $result (@results) {
37 my ($host, @data) = @$result;
38 $self->_store($host, $group, +{ map %$_, @data });
39 }
b079a95d 40 }
949dba9c 41 $self->_log('Done');
b079a95d 42 return 1;
43}
a5e1e1c6 44
b079a95d 45sub introspectors {
46 my ($self, $group) = @_;
47 return $self->config->config_for_group($group)->{introspect};
48}
60e1cc39 49
60e1cc39 50sub fetch {
b079a95d 51 my ($self, $host, $group) = @_;
52 my $spec = $self->introspectors($group);
a5e1e1c6 53 my (@sudo, @nosudo);
54 push(@{ $spec->{$_}{sudo} ? \@sudo : \@nosudo}, [$_, $spec->{$_}])
55 for sort keys %$spec;
949dba9c 56 my @futures;
a5e1e1c6 57 if (@nosudo) {
949dba9c 58 $self->_log("Without sudo: ", join ", ", map $_->[0], @nosudo);
59 my $proxy = $self->_create_gatherer(
60 host => $host,
61 introspectors => [@nosudo],
62 );
63 push @futures, $proxy->start::gather_all;
a5e1e1c6 64 }
65 if (@sudo) {
949dba9c 66 $self->_log("With sudo: ", join ", ", map $_->[0], @nosudo);
67 my $proxy = $self->_create_gatherer(
68 sudo => 1,
69 host => $host,
70 introspectors => [@sudo],
71 );
72 push @futures, $proxy->start::gather_all;
60e1cc39 73 }
949dba9c 74 return @futures;
60e1cc39 75}
76
b079a95d 77sub storage {
78 my ($self, @path) = @_;
79 my $storage = File::Tree::Snapshot->new(
80 allow_empty => 0,
81 storage_path => join('/', $self->root, @path),
82 );
83 $storage->create
84 unless $storage->exists;
85 return $storage;
60e1cc39 86}
87
88sub _store {
949dba9c 89 my ($self, $host, $group, $gathered) = @_;
90 my $storage = $self->storage($host, $group);
91 my $ok = eval {
b079a95d 92 my @files;
93 for my $class (sort keys %$gathered) {
94 my $file = sprintf '%s.json', join '/',
95 map lc, map {
96 s{([a-z0-9])([A-Z])}{${1}_${2}}g;
97 $_;
98 } split m{::}, $class;
99 my $fh = $storage->open('>:utf8', $file, mkpath => 1);
949dba9c 100 my $full_path = $storage->file($file);
101 $self->_log("Writing $full_path");
b079a95d 102 print $fh encode_json($gathered->{$class});
949dba9c 103 push @files, $full_path;
b079a95d 104 }
105 $self->_cleanup($storage, [@files]);
949dba9c 106 $self->_log("Committing");
b079a95d 107 $storage->commit;
949dba9c 108 };
109 unless ($ok) {
110 $self->_log("Rolling back snapshot because of: ", $@ || 'unknown error');
111 $storage->rollback;
112 die $@;
60e1cc39 113 }
60e1cc39 114 return 1;
115}
116
117sub _cleanup {
b079a95d 118 my ($self, $storage, $known_files) = @_;
60e1cc39 119 my %known = map { ($_ => 1) } @$known_files;
b079a95d 120 my @files = $storage->find_files('json');
60e1cc39 121 for my $file (@files) {
122 next if $known{$file};
949dba9c 123 $self->_log("Removing $file");
60e1cc39 124 unlink($file)
125 or die "Unable to remove '$file': $!\n";
126 }
127 return 1;
128}
129
130sub _create_gatherer {
a5e1e1c6 131 my ($self, %arg) = @_;
132 return System::Introspector::Gatherer->new_from_spec(
949dba9c 133 user => $self->user,
134 host => $arg{host},
135 sudo_user => $arg{sudo} && $self->sudo_user,
136 introspectors => $arg{introspectors},
a5e1e1c6 137 );
60e1cc39 138}
139
1401;
cd5c3d43 141
142=head1 NAME
143
144System::Introspector::State - Gather system state
145
146=head1 SYNOPSIS
147
148 my $state = System::Introspector::State->new(
149 host => 'foo.example.com',
150 storage => $storage_obj,
151 config => {
152 introspect => [qw( ProbeName )],
153 },
154 );
155
156 my $data = $state->fetch;
157 $state->fetch_and_store;
158
159=head1 DESCRIPTION
160
161Gathers system introspection data based on configuration and stores
162it with a L<File::Tree::Snapshot> object.
163
164=head1 ATTRIBUTES
165
166=head2 config
167
168A hash reference containing a C<introspect> key with an array reference
169value containing a list of probe names without the
170C<System::Introspector::Probe::> prefix. This attribute is required.
171
172=head2 host
173
174An optional hostname. If no hostname is supplied, the local configuration
175data will be fetched.
176
177=head2 storage
178
179A L<File::Tree::Snapshot> object.
180
181=head1 METHODS
182
183=head2 fetch
184
185 my $data = $state->fetch;
186
187Fetches all probe data.
188
189=head2 fetch_and_store
190
191 $state->fetch_and_store;
192
193Fetches all probe data and stores it in the L</storage>.
194
195=cut