diff --git a/.gitignore b/.gitignore index c15b52a..24d8e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ META.yml MYMETA.yml nytprof.out pm_to_blib +wiki/ diff --git a/NOMS/JCEL.pm b/NOMS/JCEL.pm new file mode 100644 index 0000000..f3081b2 --- /dev/null +++ b/NOMS/JCEL.pm @@ -0,0 +1,445 @@ +#!perl + +package NOMS::JCEL; +use strict; +use Data::Dumper; +use Carp; + +# TODO +# Need a way to express (field1 = 'v1' OR field2 = 'v2'). The current +# assumption is that this will be handled by the "statement" oriented +# language surrounding this expression language; for example pattern/action +# statements where two patterns lead to the same action. The problem +# with incorporating operators at the higher level is that a construct +# like { "or": [condition_list] } and { "and": [condition_list] } could +# refer to object attributes called "or" and "and" + +=head1 NAME + +NOMS::JCEL - JSON (or Jeremy's) Condition Expression Language + +=head1 SYNOPSIS + + my $obj = from_json('{ "service": "http", "status": "up" }'); + my $jcel = NOMS::JCEL->new(from_json('{ "service": ["http", "ftp"] }')); + + print "Can use for file transfer" if $jcel->test($obj); + +=head1 DESCRIPTION + +This module implements an expression language you can use to implement +conditional logic expressed as a "mapping" (like that resulting from +deserializing JSON). + +=head2 Methods + +=over 4 + +=item new + + NOMS::JSON->new($expression[, $options]); + +The expression is a hash reference to the JCEL expression. Options is a +hash reference containing options and values. The only supported option is +C<'debug'>. + +=item test + + $jcel->test($object); + +Evaluate the test(s) in the expression against the object. Returns 1 (true) +or 0 (false). + +=item as_sql + + $jcel->as_sql($options); + +Print a SQL condition predicate corresponding with the conditional expression. +In a scalar context, produces just the SQL text. In a list context, produces a +list, the first member of which is the SQL text and the remaining members of +which are literal values corresponding with SQL placeholders (see +B). The $options hash reference contains the following options and +values: + +=back + +=over 8 + +=item literals + +If true, prints the values as quoted literals. *Note:* it is possible to cause +errors; this module's attempt to quote literals is not perfect. It's safer to +leave this value alone (false). + +=back + +=head2 Language + +=head3 Conditions + + condition ::= { attribute: rvalue } + condition :: = [ condition, ... ] + +Each condition consists of a key, the object attribute to examine for a match, +and the rvalue, or comparison, to make against the value in the object +corresponding to the key. + +And empty condition list is always true in tests, and returns an empty SQL +predicate (which if plugged into an SQL statement without examination, may +cause a syntax error). + +=head3 Rvalues + + rvalue ::= literal + rvalue ::= { operator: literal } + rvalue ::= [ rvalue, ... ] + +=head3 Operator + +=over 4 + +=item = + +General-purpose string comparison. The rvalue may contain shell-like glob +patterns (C<*>, C and C<[]>). Numbers are stringified and compared as +strings. + +=item != + +Negation of B<=>. + +=item eq + +String equality comparsion. No wildcards are allowed. + +=item ne + +String inequality comparison. + +=item ~ + +Regular expression matching. The rvalue is a Perl regular expression which is +matched against the lvalue. + +=item !~ + +Negation of regular expression matching. + +=item == + +=item E + +=item E + +=item EE + +=item E= + +=item E= + +Numerical comparisons. + +=back + +=head2 Null Values + +In testing, JCEL considers two null values (as well as the case where the +object attribute does not exist) equivalent. Thus: + + my $jcel = NOMS::JCEL->new(from_json('{ "state": null }')); + +This condition will evaluate to true when testing either a hash where the +B key is C or where the B key does not exist. + +=head1 KNOWN ISSUES + +The B method is new and experimental. There are some cases where it +will not produce syntactically correct SQL, such as for an empty condition. + +=head1 AUTHOR + +Jeremy Brinkley, Ejbrinkley@evernote.comE + +=cut + +use vars qw($me $VERSION); + +BEGIN { + $me = 'NOMS::JCEL'; + $VERSION = '__VERSION__'; +} + +sub eql { + my ($lvalue, $rvalue) = @_; + + my $p = glob2pat($rvalue); + + return 1 if $lvalue =~ /$p/; + + return 1 if !defined($lvalue) and !defined($rvalue); + + return 0; +} + + +# http://www.perlmonks.org/?node_id=708493 +sub glob2pat { + my $globstr = shift; + my %patmap = ( + '*' => '.*', + '?' => '.', + '[' => '[', + ']' => ']', + '-' => '-', + ); + + $globstr =~ s{(?:^|(?<=[^\\]))(.)} { $patmap{$1} || "\Q$1" }ge; + + my $pattern = '^' . $globstr . '$'; + + return $pattern; +} + +sub isglob { + my ($g) = @_; + my $is = 0; + + $is = 1 if $g =~ /^[\?\*]/; + $is = 1 if $g =~ /[^\\][\?\*]/; + + return $is; +} + +sub glob2sql { + my ($g) = @_; + my %map = ( + '_' => '\_', + '%' => '\%', + '*' => '%', + '?' => '_' + ); + $g =~ s{(?:^|(?<=[^\\]))(.)} { $map{$1} || $1 }ge; + + return $g; +} + + +sub sqlquote { + my ($s) = @_; + + $s =~ s/\'/\\'/g; + + return "'" . $s . "'"; +} + +sub new { + my ($class, $condition, $opt) = @_; + + my $self = bless({}, $class); + + $self->{'condition'} = $condition; + $self->dbg("init condition: " . ddump($condition)); + $self->{'options'} = $opt; + $self->{'op'} = { + '=' => sub { eql($_[0], $_[1]) }, + '!=' => sub { not eql($_[0], $_[1]) }, + 'eq' => sub { $_[0] eq $_[1] }, + 'ne' => sub { $_[0] ne $_[1] }, + '==' => sub { $_[0] == $_[1] }, + '<>' => sub { $_[0] != $_[1] }, + '~' => sub { $_[0] =~ /$_[1]/ }, + '!~' => sub { $_[0] !~ /$_[1]/ }, + '>' => sub { $_[0] > $_[1] }, + '<' => sub { $_[0] < $_[1] }, + '>=' => sub { $_[0] >= $_[1] }, + '<=' => sub { $_[0] <= $_[1] } + }; + + $self->{'sqlop'} = { + '=' => sub { isglob($_[1]) ? ($_[0] . ' LIKE ?', glob2sql($_[1])) : + ($_[0] . ' = ?', $_[1]) }, + '!=' => sub { isglob($_[1]) ? ($_[0] . ' NOT LIKE ?', glob2sql($_[1])) : + ($_[0] . ' != ?', $_[1]) }, + 'eq' => sub { ($_[0] . ' = ?', $_[1]) }, + 'ne' => sub { ($_[0] . ' != ?', $_[1]) }, + '==' => sub { ($_[0] . ' = ?', $_[1]) }, + '<>' => sub { ($_[0] . ' != ?', $_[1]) }, + '>' => sub { ($_[0] . ' > ?', $_[1]) }, + '<' => sub { ($_[0] . ' < ?', $_[1]) }, + '>=' => sub { ($_[0] . ' >= ?', $_[1]) }, + '<=' => sub { ($_[0] . ' <= ?', $_[1]) }, + '~' => sub { ($_[0] . ' REGEXP ?', $_[1]) }, + '!~' => sub { ($_[0] . ' NOT REGEXP ?', $_[1]) } + }; + + $self->{'sqlop-literal'} = { + '=' => sub { isglob($_[1]) ? + $_[0] . ' LIKE ' . sqlquote(glob2sql($_[1])) : + $_[0] . ' = ' . sqlquote($_[1]) }, + '!=' => sub { isglob($_[1]) ? + $_[0] . ' NOT LIKE ' . sqlquote(glob2sql($_[1])) : + $_[0] . ' = ' . sqlquote($_[1]) }, + 'ne' => sub { $_[0] . ' != ' . sqlquote($_[1]) }, + '==' => sub { $_[0] . ' = ' . $_[1] }, + '<>' => sub { $_[0] . ' != ' . $_[1] }, + '>' => sub { $_[0] . ' > ' . $_[1] }, + '<' => sub { $_[0] . ' < ' . $_[1] }, + '>=' => sub { $_[0] . ' >= ' . $_[1] }, + '<=' => sub { $_[0] . ' <= ' . $_[1] }, + '~' => sub { $_[0] . ' REGEXP ' . sqlquote($_[1]) }, + '!~' => sub { $_[0] . ' NOT REGEXP ' . sqlquote($_[1]) } + }; + + + return $self; +} + +sub test { + my ($self, $object) = @_; + + my $condition = $self->{'condition'}; + + my $rv = 0; + eval { + $rv = $self->match_condition($condition, $object); + }; + if ($@) { + my $err = $@; + chomp($err); + $err =~ s/ at \S*JCEL.pm.*//; + carp $err; + } + + return $rv; +} + +sub as_sql { + my ($self, $sqlopt) = @_; + my $literals = 0; + + $literals = 1 if (defined($sqlopt) and $sqlopt->{'literals'}); + + my ($st, @literals) = $self->sql($self->{'condition'}, + { 'literals' => $literals }); + + return (wantarray ? ($st, @literals) : $st); +} + +sub sql { + my ($self, $condition, $options, @literals) = @_; + my $sqltext = ''; + + if (ref($condition) eq 'ARRAY') { + my @subsqltexts = (); + for my $subcondition (@{$condition}) { + my ($subsqltext, @sublits) = $self->sql($subcondition, $options); + push(@subsqltexts, $subsqltext); + push(@literals, @sublits); + } + $sqltext = join(' AND ', @subsqltexts); + } else { + my ($lvalue) = keys(%$condition); + my $rvalue = $condition->{$lvalue}; + my $op = '='; + my $optype = ($options->{'literals'} ? 'sqlop-literal' : 'sqlop'); + if (ref($rvalue)) { + if (ref($rvalue) eq 'ARRAY') { + $op = 'IN'; + my $rvaluetext = $options->{'literals'} ? + '(' . join(', ', map { sqlquote($_) } @$rvalue) . ')' : + '(' . join(', ', map { '?' } @$rvalue) . ')'; + $sqltext = join(' ', $lvalue, $op, $rvaluetext); + push(@literals, @$rvalue); + } elsif (ref($rvalue) eq 'HASH') { + ($op) = keys(%$rvalue); + $rvalue = $rvalue->{$op}; + ($sqltext, $rvalue) = $self->{$optype}->{$op}->($lvalue, $rvalue); + push(@literals, $rvalue); + } + } else { + ($sqltext, $rvalue) = $self->{$optype}->{$op}->($lvalue, $rvalue); + push(@literals, $rvalue); + } + } + + return ($options->{'literals'} ? ($sqltext) : ($sqltext, @literals)); +} + +sub match_condition { + my ($self, $condition, $object) = @_; + + $self->dbg("match_condition(" . ddump($condition) . ", " . ddump($object) + . ')'); + + # and + if (ref($condition) eq 'ARRAY') { + for my $subcondition (@$condition) { + return 0 unless $self->match_condition($subcondition, $object); + } + return 1; + } + + if (! ref($condition) or ref($condition) ne 'HASH') { + die "Condition must be HASH or ARRAY reference"; + } + + return 1 unless keys %$condition; # empty condition is true + + for my $attr (keys %$condition) { + my $rvalue = $condition->{$attr}; + return 0 unless $self->match_rvalue($rvalue, $object->{$attr}); + } + + return 1; +} + +sub match_rvalue { + my ($self, $rvalue, $lvalue) = @_; + + $self->dbg("match_rvalue(" . ddump($rvalue) . ", " . ddump($lvalue) . ')'); + + if (ref($rvalue)) { + if (ref($rvalue) eq 'HASH') { + my ($op) = keys %$rvalue; + $self->dbg(" op is: " . ddump($op)); + my $simple_rvalue = $rvalue->{$op}; + if (ref($simple_rvalue)) { + die "rvalue of explicit operator must be simple"; + } + return tv($self->{'op'}->{$op}->($lvalue, $simple_rvalue)); + } elsif (ref($rvalue) eq 'ARRAY') { + $self->dbg(" rvalue is list"); + for my $simple_rvalue (@$rvalue) { + if (ref($simple_rvalue)) { + die "each rvalue in rvalue list must be simple"; + } + $self->dbg(" checking simple rvalue: " + . ddump($simple_rvalue)); + return 1 if $self->{'op'}->{'='}->($lvalue, $simple_rvalue); + } + return 0; + } + } else { + $self->dbg(" rvalue is simple: $rvalue"); + return tv($self->{'op'}->{'='}->($lvalue, $rvalue)); + } +} + +sub ddump { + my $var = 'var0'; + Data::Dumper->new([@_],[map {$var++} @_])->Terse(1)->Indent(0)->Dump; +} + +sub dbg { + my ($self, @msg) = @_; + print STDERR "DBG($me): ", join("\nDBG($me): ", @msg), "\n" + if $self->{'options'}->{'debug'}; +} + +sub tv { + my ($val) = @_; + return 1 if $val; + return 0; +} + +1; diff --git a/README.md b/README.md index 6c27a8d..58a4dc5 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,13 @@ cmdb-api ======== CMDB API Server + +The inventory CMDB started as way to track assets and some parts of configuration of these assets, and evolved (for better or for worse) to be able to manage and track non-physical assets (VMs, VIPs, etc...) and also to manage non-system configuration (service, cluster instance, etc...) + +It is now a part of a larger set of loosely coupled Operations tools that we collectively refer to as NOMS: +https://github.com/evernote/noms-client/wiki + +The API works very well with the UI project +CMDB-UI (https://github.com/isaacfinnegan/cmdb-ui) - Web-based GUI for the NOMS CMDB + +More information can be found on the cmdb api wiki: https://github.com/isaacfinnegan/cmdb-api/wiki diff --git a/clean b/clean new file mode 100755 index 0000000..3655622 --- /dev/null +++ b/clean @@ -0,0 +1,3 @@ +#!/bin/bash + +rm -f *.tar.gz diff --git a/cmdb_api.conf b/cmdb_api.conf index e25b4db..d7e09e3 100644 --- a/cmdb_api.conf +++ b/cmdb_api.conf @@ -8,13 +8,14 @@ "prism_domain":"<%=prism_domain%>", "logconfig":"/var/www/cmdb_api/log4perl.conf", "lexicon":"/var/www/cmdb_api/pp_lexicon.xml", + "ipaddress_attribute":"ipaddress", + "traffic_control_search_fields" : ["fqdn","macaddress","ipaddress"], "entities":{ "acl":"Acl", "vip":"Generic", "datacenter_subnet":"Generic", "data_center":"Generic", "role":"Generic", - "pod_cluster":"Generic", "snat":"Generic", "pool":"Generic", "cluster":"Generic", @@ -22,21 +23,11 @@ "cluster_mta":"Generic", "system":"System", "device":"System", - "blade_chassis":"System", - "console_server":"System", - "firewall":"System", - "load_balancer":"System", - "network_switch":"System", - "power_strip":"System", - "router":"System", - "storage_head":"System", - "storage_shelf":"System", - "device_ip":"Generic", "newhostname":"Provision", "pcmsystemname":"ProvisionPcm", "user":"Generic", "currentUser":"User", - "inv_audit":"Generic", + "inv_audit":"Audit", "audit":"Audit", "inv_normalizer":"Generic", "fact":"TrafficControl", @@ -48,6 +39,7 @@ "instance_location":"Generic", "rack": "Generic", "column_lkup": "Column_lkup", - "environments":"Environments" + "environments":"Environments", + "environmentservice": "Environments" } } diff --git a/cmdb_api.pm b/cmdb_api.pm index 7d08d01..d47c4e9 100644 --- a/cmdb_api.pm +++ b/cmdb_api.pm @@ -19,8 +19,6 @@ package cmdb_api; use strict; use warnings; -use lib '/opt/pptools'; -use ppenv; use URI::Escape; use Apache2::RequestRec (); use Apache2::Request; @@ -28,2674 +26,3074 @@ use Apache2::RequestIO (); use Apache2::Connection; use Apache2::Access; use APR::Brigade (); -use APR::Bucket (); +use APR::Bucket (); use Log::Log4perl qw(:easy); use Apache2::Filter (); use Apache2::Const -compile => qw(OK HTTP_NOT_FOUND HTTP_OK HTTP_FAILED_DEPENDENCY HTTP_NOT_ACCEPTABLE HTTP_NO_CONTENT HTTP_INTERNAL_SERVER_ERROR DECLINED HTTP_ACCEPTED HTTP_CREATED HTTP_UNAUTHORIZED SERVER_ERROR MODE_READBYTES HTTP_CONFLICT HTTP_FORBIDDEN HTTP_METHOD_NOT_ALLOWED); -use APR::Const -compile => qw(SUCCESS BLOCK_READ); -use constant IOBUFSIZE => 8192; +use APR::Const -compile => qw(SUCCESS BLOCK_READ); +use constant IOBUFSIZE => 8192; use JSON; -use XML::Parser; -use XML::Simple; use Apache::DBI; +use XML::Simple; use Date::Manip; use Optconfig; use DBI; +use Data::Dumper; +use NOMS::JCEL; + +sub eat_json +{ + my ( $json_text, $opthash ) = @_; + return ( $JSON::VERSION > 2.0 ? from_json( $json_text, $opthash ) : JSON->new()->jsonToObj( $json_text, $opthash ) ); +} + +sub make_json +{ + my ( $obj, $opthash ) = @_; + return ( $JSON::VERSION > 2.0 ? to_json( $obj, $opthash ) : JSON->new()->objToJson( $obj, $opthash ) ); +} + +my $opt = Optconfig->new( + 'cmdb_api', + { + 'driver=s' => 'mysql', + 'dbuser=s' => 'dbuser', + 'dbpass=s' => 'dbpass', + 'dbhost=s' => 'localhost', + 'database' => 'inventory', + 'debug' => 1, + 'prism_domain=s' => 'prism.ppops.net', + 'logconfig=s' => '/var/www/cmdb_api/log4perl.conf', + 'lexicon=s' => '/var/www/cmdb_api/lexicon.json', + 'ipaddress_attribute=s' => "ip_address", + "traffic_control_search_fields" => [ "fqdn", "macaddress", "ipaddress" ], + 'entities' => { + acl => 'Acl', + vip => 'Generic', + datacenter_subnet => 'Generic', + data_center => 'Generic', + role => 'Generic', + snat => 'Generic', + pool => 'Generic', + cluster => 'Generic', + hardware_model => 'Generic', + cluster_mta => 'Generic', + system => 'System', + device => 'System', + newhostname => 'Provision', + pcmsystemname => 'ProvisionNcc', + nccsystemname => 'ProvisionNcc', + user => 'Generic', + currentUser => 'User', + inv_audit => 'Audit', + audit => 'Audit', + inv_normalizer => 'Generic', + fact => 'TrafficControl', + change_queue => 'ChangeQueue', + service_instance => 'Generic', + service_instance_data => 'Generic', + instance_size => 'Generic', + instance_location => 'Generic', + column_lkup => 'Column_lkup', + environments => 'Environments', + environmentservice => 'Environments' + } + } +); + +my $valid_entity_apis = { + 'Environments' => 1, + 'ChangeQueue' => 1, + 'System' => 1, + 'Generic' => 1, + 'Audit' => 1 +}; + +my $DEBUG = $opt->{'debug'}; +my $DBHOST = $opt->{'dbhost'}; +my $DBUSER = $opt->{'dbuser'}; +my $DBPASS = $opt->{'dbpass'}; +my $DATABASE = $opt->{'database'}; +my $IPADDRESSFIELD = $opt->{'ipaddress_attribute'}; +my $DRIVER = $opt->{'driver'}; +my ( $lexicon, $tree, $parser ); +my ($parms); +my $log_config_file = $opt->{'logconfig'}; + +Log::Log4perl::init($log_config_file); + +my $logger = Log::Log4perl->get_logger('inventory.cmdb_api'); +my $syslog = Log::Log4perl->get_logger('inventory.syslog'); + +unless ($lexicon) +{ + #TODO this hardcoded path is bad fix it + $lexicon = $opt->{'lexicon'}; +} + +# database connection +our $dbh; + +# apache request +our $r; + +our $db_retry=0; + +#valid api types. these must exist and be parsable in the lexicon if they are 'Generic' +# or have provided doGET/PUT/POST functions + +my $valid_entities = $opt->{'entities'}; + +my $versions = ['v1']; + +eval { + if (open (my $json_str, $lexicon)) + { + local $/ = undef; + $tree = eat_json(<$json_str>); + close($json_str); + } +}; + +# show error and die if parsing of the lexicon failed +if ($@) +{ + $logger->fatal("error parsing $lexicon\n$@"); + exit; +} + +$logger->info("$lexicon is xml ok"); + +our $tree_extended; +$tree_extended = &eat_json( &make_json($tree) ); + +# loop through entities and add base attributes to things that subclass other stuff +foreach ( keys( %{ $tree_extended->{entities} } ) ) +{ + if ( $tree_extended->{entities}->{$_}->{_extends} ) + { + my $extends = &lkupLexiconPath( $tree->{entities}->{$_}->{_extends} ); + foreach my $attr ( keys(%$extends) ) + { + $tree_extended->{entities}->{$_}->{$attr} = $extends->{$attr}; + } + } +} +$logger->debug( "lexicon: " . &make_json( $tree, { pretty => 1, allow_nonref => 1 } ) ) if ( $logger->is_debug() ); +$logger->debug( "lexicon extended: " . &make_json( $tree_extended, { pretty => 1, allow_nonref => 1 } ) ) if ( $logger->is_debug() ); + +sub lkupLexiconPath() +{ + my $str = shift; + my @seg = split( '/', $str ); + shift(@seg); + shift(@seg); + my $rtn = '$tree->{' . shift(@seg) . '}'; + foreach (@seg) + { + $rtn .= '->{' . $_ . '}'; + } + return eval($rtn); +} + +# mod perl2 handler +sub handler() +{ + $dbh = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS ); + $r = shift; + my $up_uri = $r->unparsed_uri(); + $up_uri =~ s/.+\?//; + my $uri = uri_unescape($up_uri); + my $req = Apache2::Request->new($r); + my $connection = $r->connection; + my ( $requestObject, $data, $formatted_data ); + %{ $$requestObject{'query'} } = %{ $req->param } if $req->param; + $$requestObject{'getparams'} = $uri; + $$requestObject{'stat'} = Apache2::Const::HTTP_OK; + $$requestObject{'_format'} = $$requestObject{'query'}{'_format'} || 'json'; + $$requestObject{'method'} = $req->method(); + @{ $$requestObject{'path'} } = split( '/', $req->uri() ); + $$requestObject{'pathstr'} = $req->uri(); + $$requestObject{'user'} = &doGenericGET( { entity => 'user', path => [ $req->user ] } ) if $req->user; + if($db_retry) + { + $db_retry=0; + $logger->error("(handler 1)detected bad db handle, redirecting to self and terminating apache child"); + $syslog->error("(handler 1)detected bad db handle, redirecting to self and terminating apache child"); + $r->headers_out->set('Location' => ($r->subprocess_env('HTTPS') eq 'on' ? 'https' : 'http' ) . '://' . $r->hostname() . $r->unparsed_uri() ); + no strict 'subs'; + $r->status(302); + $r->child_terminate(); + return Apache2::Const::REDIRECT; + } + + $$requestObject{'http_auth_user'} = $req->user if $req->user; + $$requestObject{'request_object'} = $r; + # apache 2.4 doesn't have this anymore + if( $connection->can('remote_ip') ) + { + $$requestObject{'ip_address'} = $connection->remote_ip(); + } + else + { + $$requestObject{'ip_address'} = $r->useragent_ip; + } + if ( $$requestObject{'method'} ne 'GET' ) + { + $$requestObject{'body'} = read_post($r); + } + else + { + $$requestObject{'body'} = ''; + } + shift( @{ $$requestObject{'path'} } ); + if ( shift( @{ $$requestObject{'path'} } ) eq 'cmdb_api' ) + { + $$requestObject{'requested_api'} = shift( @{ $$requestObject{'path'} } ); + $$requestObject{'entity'} = shift( @{ $$requestObject{'path'} } ); + $logger->debug( &make_json( $requestObject, { pretty => 1, allow_nonref => 1, allow_blessed => 1 } ) ) if ( $logger->is_debug() ); + + # do help if it was asked + if ( exists $requestObject->{'query'}->{'help'} || exists $requestObject->{'query'}->{'lexicon'} ) + { + $requestObject->{'help'} = 1; + if ( !$requestObject->{'requested_api'} ) + { + $r->print( &make_json($versions) ); + return Apache2::Const::OK; + + } + elsif ( !$requestObject->{'entity'} ) + { + my $ents = []; + my $lex = {}; + if ( $requestObject->{'query'}->{'lexicon'} ) + { + foreach ( keys(%$valid_entities) ) + { + $lex->{$_} = $tree->{entities}->{$_}; + no strict 'refs'; + # check each attribute and populate enumerations if needed + foreach my $attr ( keys( %{ $lex->{$_} } ) ) + { + if ( $lex->{$_}->{$attr} + && ref( $lex->{$_}->{$attr} ) eq 'HASH' + && defined $lex->{$_}->{$attr}->{'_enumeration'} + && defined $lex->{$_}->{$attr}->{'_enumeration'}->{'entity'} + && defined $lex->{$_}->{$attr}->{'_enumeration'}->{'attribute'} ) + { + $lex->{$_}->{$attr}->{'_enumeration'}->{'enumerator'} = &doColumn_lkupGET( $requestObject, $lex->{$_}{$attr}{'_enumeration'}{'entity'}, $lex->{$_}{$attr}{'_enumeration'}->{'attribute'} ); + } + } + } + $r->print( &make_json($lex) ); + } + else + { + foreach ( keys(%$valid_entities) ) + { + push( @$ents, $_ ) if ( $valid_entity_apis->{ $valid_entities->{$_} } == 1 ); + } + $r->print( &make_json($ents) ); + } + return Apache2::Const::OK; + } + else + { + #$r->print(&make_json(&getFieldList($requestObject->{'entity'}))); + $r->print( &make_json( $tree->{entities}->{ $requestObject->{'entity'} }, { pretty => 1, allow_nonref => 1 } ) ); + return Apache2::Const::OK; + } + } + + # check for valid entity + unless ( $$requestObject{'entity'} && $$valid_entities{ $$requestObject{'entity'} } ) + { + $logger->debug("valid entities:") if ( $logger->is_debug() ); + $logger->debug("entity lkup: $$valid_entities{$$requestObject{'entity'}}") if ( $logger->is_debug() ); + $r->print('valid entity required'); + return Apache2::Const::HTTP_NOT_ACCEPTABLE; + } + + #deal with the connection and produce data + $data = &ProcessRequest($requestObject); + + if($db_retry) + { + $db_retry=0; + $logger->error("(handler 2)detected bad db handle, redirecting to self and terminating apache child"); + $syslog->error("(handler 2)detected bad db handle, redirecting to self and terminating apache child"); + $r->headers_out->set('Location' => $r->unparsed_uri() ); + no strict 'subs'; + $r->status(Apache2::Const::HTTP_MOVED_TEMPORARILY); + $r->child_terminate(); + return Apache2::Const::REDIRECT; + } + + $r->status( $$requestObject{'stat'} ); + if ( $$requestObject{'stat'} eq '500' ) + { + $data = { + success => 'false', + message => $data + }; + } + $logger->debug("final return of status: $$requestObject{'stat'}"); + + #TODO reconcile the '"string" data that comes back from above and how we output it (errors, etc...) + if ( $$requestObject{'headers_out'} ) + { + $r->headers_out->add( $$requestObject{'headers_out'}[0] => $$requestObject{'headers_out'}[1] ); + } + if(defined $data && $data eq 'killchild') + { + $r->child_terminate(); + undef $data; + } + + #TODO make output format based on accept content header + if ( !defined $data && keys( %{ $$requestObject{'query'} } ) > 0 ) + { + $data = []; + } + + # set output format + if ( defined $data ) + { + if ( $$requestObject{'_format'} eq 'json' ) + { + $r->content_type('application/json'); + $formatted_data = &make_json( $data, { pretty => 1, allow_nonref => 1, allow_blessed => 1 } ); + } + elsif ( $$requestObject{'_format'} eq 'xml' ) + { + $r->content_type('text/xml'); + $formatted_data = XMLout($data); + } + elsif ( $$requestObject{'_format'} eq 'text' ) + { + $logger->debug("output data as text") if ( $logger->is_debug() ); + $formatted_data = $data; + } + $r->print($formatted_data); + } + } + else + { + $logger->error("error parsing api str"); + } + return Apache2::Const::OK; +} + +sub doFieldNormalization() +{ + my ( $entity, $field, $value ) = @_; + my $newvalue; + $value =~ s/^\ //g if defined $value; + $value =~ s/\ $//g if defined $value; + my $matchers = $dbh->selectall_arrayref( 'select matcher,sub_value from inv_normalizer where entity_name=? and field_name=?', {}, ( $entity, $field ) ); + foreach (@$matchers) + { + + if ( $value =~ m/$$_[0]/i ) + { + $logger->debug("matched with $$_[0] and subbing $$_[1]") if ( $logger->is_debug() ); + return $$_[1]; + } + } + if ( ref $value eq 'ARRAY' ) + { + $value = join( ',', @$value ); + } + return $value; +} + +# lifted from mod_perl2 docs, does body content read for post/put +sub read_post +{ + my $r = shift; + my $bb = APR::Brigade->new( $r->pool, $r->connection->bucket_alloc ); + my $data = ''; + my $seen_eos = 0; + do + { + $r->input_filters->get_brigade( $bb, Apache2::Const::MODE_READBYTES, APR::Const::BLOCK_READ, IOBUFSIZE ); + for ( my $b = $bb->first ; $b ; $b = $bb->next($b) ) + { + if ( $b->is_eos ) + { + $seen_eos++; + last; + } + if ( $b->read( my $buf ) ) + { + $data .= $buf; + } + $b->remove; # optimization to reuse memory + } + } while ( !$seen_eos ); + $bb->destroy; + return $data; +} + +#processes lexicon to get fields for an entity +sub getFieldList() +{ + my $entity = shift; + my $bare = shift || 0; + my @arr; + foreach ( keys( %{ $tree->{entities}->{$entity} } ) ) + { + next if ( $_ =~ /^_/ || $_ eq 'key' || $_ eq 'extends' || $_ eq 'table' || $_ eq 'field_order'); + push( @arr, $_ ); + } + if ( $entity eq 'system' && !$bare ) + { + foreach ( keys( %{ $tree->{entities}->{device} } ) ) + { + next if ( $_ =~ /^_/ || $_ eq 'key' || $_ eq 'extends' || $_ eq 'table' || $_ eq 'field_order' ); + push( @arr, $_ ); + } + } + $logger->info( "processed fields for $entity : " . join( ',', @arr ) ); + return \@arr; +} + +sub runACL() +{ + my ( $req, $r, $entity, $changes, $blocked_changes ) = @_; + my ($groups) = $req->{'user'}->{'groups'}; + if ( ref $groups ne 'ARRAYREF' ) + { + $logger->debug( "groups ref= " . ref $groups ) if ( $logger->is_debug() ); + $groups = [ split( ',', $groups ) ] if $groups; + } + my $acls = $dbh->selectall_arrayref( "select * from acl where entity=?", { Slice => {} }, ($entity) ); + foreach my $field ( keys(%$changes) ) + { + foreach my $acl (@$acls) + { + #skip if acl group not in users grouplist + next unless ( grep( /^$acl->{'acl_group'}$/, @$groups ) ); + + # skip of the field the acl applies to isn't being changed + next unless ( $field eq $acl->{'field'} || $acl->{'field'} eq '*' ); + $logger->info( "found acl to process: " . &make_json($acl) ); + my $eval = $acl->{'logic'}; + my $out = &doEval( $req, $r, $field, $changes, $acl->{'logic'} ); + if ($@) + { + die 'error compiling ACL'; + } + if ($out) + { + $logger->info("acl ran and blocked"); + $blocked_changes->{$field} = $changes->{$field}; + delete $changes->{$field}; + } + } + } + return ( $changes, $blocked_changes ); +} + +sub doEval() +{ + my ( $req, $r, $f, $changes, $logic ) = @_; + + #$logger->debug("ACL EVAL: $logic ") + return eval($logic); +} + +# looks for function to process the request, based on entity specification in $valid_entities and http method +sub ProcessRequest() +{ + $logger->info("detemining request process function"); + + my $requestObject = shift; + my $func = 'do' . $$valid_entities{ $$requestObject{'entity'} } . $$requestObject{'method'}; + if ( $$requestObject{'method'} eq 'PUT' ) + { + unless ( $$requestObject{'path'}[0] ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return 'no key specified'; + } + + #$$requestObject{'stat'}=Apache2::Const::HTTP_ACCEPTED; + } + if ( $$requestObject{'method'} eq 'POST' ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_CREATED; + } + no strict 'refs'; + if ( exists &$func ) + { + $logger->info("found function $func"); + return &$func($requestObject); + } + else + { + $logger->error("no function found $func"); + $$requestObject{'stat'} = Apache2::Const::HTTP_METHOD_NOT_ALLOWED; + return 'no entity function'; + } +} + +sub doColumn_lkupGET() +{ + my $requestObject = shift; + my $entity = shift || $$requestObject{'path'}[0]; + my $col = shift || $$requestObject{'path'}[1]; + + my %lkup; + if ( $entity eq 'system' ) + { + $entity = 'device'; + } + my $entity_fields = &getFieldList($entity); + $logger->info( "$entity field list: " . join( ',', @$entity_fields ) ); + foreach (@$entity_fields) { $lkup{$_}++; } + $logger->info( 'doing col lkup for ' . $entity . '-> ' . $col ); + + my $sql; + if ( $lkup{$col} ) + { + $sql = "select distinct $col,? from $entity order by 1 limit 2000"; + } + else + { + $sql = 'select distinct metadata_value from device_metadata where metadata_name=? order by 1 limit 2000'; + } + my $res = $dbh->selectcol_arrayref( $sql, {}, ($col) ); + my @return; + + # only adds value to return set if non empty after removing quotes + foreach my $ll (@$res) { + if(defined($ll)) + { + $ll =~ s/\"//g; + push( @return, $ll ); + } + } + + return \@return; +} + +#audit info retreival + +sub doAuditGET() +{ + my $requestObject = shift; + return doGenericGET($requestObject); +} + +#special api to check current user for write access +sub doUserGET() +{ + my $requestObject = shift; + if ( $requestObject->{'user'} ) + { + return $requestObject->{'user'}; + } + else + { + return { username => $requestObject->{'http_auth_user'} }; + } +} + +# new traffic control api +sub doTrafficControlPOST() +{ + my $requestObject = shift; + $requestObject->{'user'} = &doGenericGET( { entity => 'user', path => ['trafficcontrol'] } ); + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + my ( $lkup_data, $lkup ); + $logger->debug("TC got POST from agent, finding system with fields: " . join(',',@{ $opt->{'traffic_control_search_fields'} }) ) if ( $logger->is_debug() ); + + foreach ( @{ $opt->{'traffic_control_search_fields'} } ) + { + # skip serial if not dell tag + next if ( $_ eq 'serial_number' && length( $data->{$_} ) < 4 ); + $lkup = $dbh->selectall_arrayref( "select * from device where $_=?", { Slice => {} }, ( $data->{$_} ) ); + if ( scalar(@$lkup) == 1 ) + { + $lkup_data = $$lkup[0]; + $logger->debug("TC found system with $_ : $data->{$_}" ); + last; + } + } + if ( ref $lkup_data eq 'ARRAY' && scalar(@$lkup_data) == 0 ) + { + $lkup_data = ''; + } + $requestObject->{'entity'} = 'system'; + + # loop through the entity field and assign values based on 'fact' designation of attributes + # or default to just looking for an entry of the same name + my $data_assembled = { 'fqdn' => $data->{'fqdn'} }; + foreach my $attr ( keys( %{ $tree_extended->{'entities'}->{'system'} } ) ) + { + if ( ref( $tree_extended->{'entities'}->{'system'}->{$attr} ) eq 'HASH' && $tree_extended->{'entities'}->{'system'}->{$attr}->{'_fact'} ) + { + foreach my $fact_lookup ( split( ',', $tree_extended->{'entities'}->{'system'}->{$attr}->{'_fact'} ) ) + { + $logger->debug("doing fact lookup for $attr with $fact_lookup"); + if ( $data->{$fact_lookup} ) + { + $data_assembled->{$attr} = $data->{$fact_lookup}; + $logger->debug("found data lookup for $attr in fact $fact_lookup: $data->{$fact_lookup}"); + last; + } + } + } + else + { + $data_assembled->{$attr} = $data->{$attr} if ( $data->{$attr} ); + } + } + $requestObject->{'body'} = make_json( $data_assembled, { allow_nonref => 1 } ); + + # if we found the entry, then setup for PUT else do POST + if ($lkup_data) + { + $logger->info( "TC found system " . $lkup_data->{'fqdn'} . ", doing PUT" ); + $requestObject->{'path'} = [ $lkup_data->{'fqdn'} ]; + &doSystemPUT($requestObject); + } + else + { + $logger->info("TC doing POST for new system"); + &doSystemPOST($requestObject); + } + +} + +sub doProvisionNccGET() +{ + my $requestObject = shift; + my $id = $$requestObject{'path'}[0]; + + if ( !$id ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_ACCEPTABLE; + return { 'error' => 'missing data (id)' }; + } + + my ( $sql, $sth, $existing_data ); + $sql = 'select fqdn from device where serial_number=?'; + $sth = $dbh->prepare($sql); + $sth->execute($id); + $existing_data = $sth->fetchall_arrayref( {}, undef ); + + if ( scalar(@$existing_data) > 1 ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return { 'error' => 'Multiple systems with the same ID' }; + } + elsif ( scalar(@$existing_data) ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_OK; + return { 'fqdn' => $$existing_data[0]{'fqdn'} }; + } + + my $record = {}; + $$record{'inventory_component_type'} = 'system'; + $$record{'status'} = 'idle'; + $$record{'serial_number'} = $id; + + my $name = &setNewName( $record, '.' . $opt->{'prism_domain'} ); + + $$requestObject{'stat'} = Apache2::Const::HTTP_OK; + return { 'fqdn' => $name }; +} + +# special api to fetch new hostname for provisioning +# this API is deprecated +sub doProvisionGET() +{ + my $requestObject = shift; + my ( $sql, $sth, $rv ); + + $logger->warn("doProvisionGET API is deprecated and should not longer be used"); + + # this code seems pointless, why check for length 7 if we are going to remove chars? + if ( $$requestObject{'query'}{'serial_number'} && length( $$requestObject{'query'}{'serial_number'} ) == 7 ) + { + $$requestObject{'query'}{'serial_number'} =~ s/^\s+//g; + $$requestObject{'query'}{'serial_number'} =~ s/\s+$//g; + } + if ( $$requestObject{'query'}{'serial_number'} && length( $$requestObject{'query'}{'serial_number'} ) == 7 ) + { + $sql = 'select fqdn from device where serial_number=?'; + $sth = $dbh->prepare($sql); + $rv = $sth->execute( ( $$requestObject{'query'}{'serial_number'} ) ); + } + elsif ( !$$requestObject{'query'}{'mac_address'} ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_FAILED_DEPENDENCY; + return 'missing data (mac_address)'; + } + else + { + $sql = 'select fqdn from device where mac_address=?'; + $sth = $dbh->prepare($sql); + $rv = $sth->execute( ( $$requestObject{'query'}{'mac_address'} ) ); + } + + # unless($$requestObject{'query'}{'serial_number'}) + # { + # $$requestObject{'stat'}=Apache2::Const::HTTP_FAILED_DEPENDENCY; + # return 'missing data (serial_number)'; + # } + # lkup system + my $data = $sth->fetchall_arrayref( {}, undef ); + my ( $hostname, $newname ); + + # gather data sent in with the call + my $new = {}; + foreach my $f ( ( 'rack_code', 'rack_position', 'asset_tag_number', 'manufacturer', 'product_name', 'serial_number', $IPADDRESSFIELD, 'mac_address', 'inventory_component_type' ) ) + { + $$requestObject{'query'}{$f} =~ s/^\s+//g; + $$requestObject{'query'}{$f} =~ s/\s+$//g; + if ( $$requestObject{'query'}{$f} ) + { + $$new{$f} = &doFieldNormalization( 'system', $f, $$requestObject{'query'}{$f} ); + + #$$new{$f}=$$requestObject{'query'}{$f}; + } + } + + # there should only be one system + if ( scalar(@$data) > 1 ) + { + $logger->warn( "found too many systems in inventory: " . scalar(@$data) ); + return 'too many entries'; + + #TODO do something about finding more than one system + } + elsif ( scalar(@$data) ) + { + $logger->info( "found system in inventory: " . $$data[0]{'fqdn'} ); + + # call setNewName which will rename if it doesn't match + $$new{'fqdn'} = $$data[0]{'fqdn'}; + $$new{'status'} = $$data[0]{'status'}; + $newname = &setNewName( $new, '.ppops.net' ); + + # set the IP for this system since it's coming online from provisioning vlan + # validate that the IP is on the provisioning vlan + if ( $$requestObject{'query'}{$IPADDRESSFIELD} && $$requestObject{'query'}{$IPADDRESSFIELD} =~ /10\.\d{1,3}\.25/ ) + { + $logger->info("updating IP for system entry"); + $$requestObject{'query'}{$IPADDRESSFIELD} =~ s/^\s//g; + $$requestObject{'query'}{$IPADDRESSFIELD} =~ s/\s$//g; + $sql = "update device set $IPADDRESSFIELD=? where fqdn=?"; + my $sth = $dbh->prepare($sql); + $logger->debug("executing: $sql with $$requestObject{'query'}{$IPADDRESSFIELD},$newname") if ( $logger->is_debug() ); + my $rv = $sth->execute( ( $$requestObject{'query'}{$IPADDRESSFIELD}, $newname ) ); + $logger->error( $sth->err . " : " . $sth->errstr ) if ( $sth->err ); + } + } + + # no entry found, insert system into inventory with new name + else + { + unless ( $$requestObject{'query'}{'inventory_component_type'} ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_FAILED_DEPENDENCY; + return 'missing data (inventory_component_type)'; + } + $$new{'status'} = 'idle'; + $newname = &setNewName( $new, '.ppops.net' ); + if ( $newname =~ /ERROR/ ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_FAILED_DEPENDENCY; + } + } + if ( $$requestObject{'_format'} eq 'text' ) + { + $hostname = $newname; + } + else + { + $hostname = { fqdn => $newname }; + + } + $logger->info("new name assigned : $newname"); + return $hostname; + +} + +sub setNewName() +{ + my $r = shift; + my $suffix = shift || ""; + my ( $sql, $parms, $where ); + my $newname; + + # check to see if the name is correct or if box is set to production/deployment + #TODO: this should be kicking in based on violating an ACL or all just needs to go away + if ( $$r{'fqdn'} !~ /m\d{7}\.ppops\.net/ && $$r{'status'} ne 'production' && $$r{'status'} ne 'deployment' ) + { + if ( defined $$r{'mac_address'} ) + { + $newname = 'temp-' . $$r{'mac_address'}; + $newname =~ s/://g; + } + elsif ( defined $$r{'serial_number'} ) + { + $newname = 'temp-' . $$r{'serial_number'}; + } + else + { + return "ERROR: must provide serial number or MAC address"; + } + } + else + { + $newname = $$r{'fqdn'}; + } + + # fqdn passed in, so updating existing + if ( $$r{'fqdn'} ) + { + $sql = "update device set fqdn=?"; + push( @$parms, $newname ); + } + + # otherwise trying insert + else + { + $sql = 'insert into device set fqdn=?'; + push( @$parms, $newname ); + } + foreach my $f ( keys(%$r) ) + { + if ( $$r{$f} && $f ne 'fqdn' ) + { + $sql .= ", $f=? "; + push( @$parms, $$r{$f} ); + } + } + + # doing device update so adding where clause + if ( $sql =~ /update\ device/ ) + { + $where = " where fqdn=?"; + push( @$parms, $$r{'fqdn'} ); + } + + my $dbh = DBI->connect( "DBI:mysql:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 0, RaiseError => 1 } ); + my $sth; + + eval { + $sth = $dbh->prepare("$sql$where"); + executeDbStatement( $sth, $sql, @$parms ); + + my $device_id = $sth->{mysql_insertid}; + + # We need to generate a name based on the auto-incremented + # id column if this is a new device + if ( $newname =~ /^temp-/ ) + { + $newname = sprintf "m%07d", $device_id; + $newname .= $suffix; + + $sql = "update device set fqdn=? where id=?"; + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, ( $newname, $device_id ) ); + # we also need to setup any default field values that the lexicon has + + #commit so the next edit works + $dbh->commit; + my $data=&applyDefaults({},'system'); + # assemble whats needed to call the systemPUT api to make the update + doSystemPUT( + { + 'path' => ["$newname"], + 'body' => make_json($data), + 'entity' => 'system', + 'user' => { 'systemuser' => 1, 'username' => 'lexicon_defaults' }, + 'ip_address' => 'nccsystemname' + } + ); + + } + + $dbh->commit; + }; + if ($@) + { + my $errstr; + + if ( defined $sth && $sth->err ) + { + $errstr = $sth->err . " : " . $sth->errstr; + $logger->error($errstr); + } + else + { + $errstr = $@; + } + + $newname = "ERROR: $errstr"; + } + + return $newname; +} + +sub doSql() +{ + my $sql = shift; + my $parms = shift; + my $dbh = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS ); + + if (!defined($dbh)) + { + + $logger->error("(doSql)detected bad db handle, redirecting to self and terminating apache child"); + $syslog->error("(doSql)detected bad db handle, redirecting to self and terminating apache child"); + print STDERR "detected bad db handle, redirecting to self and terminating apache child"; + # check for bad db handle and redirect to self + $r->headers_out->set('Location' => ($r->subprocess_env('HTTPS') eq 'on' ? 'https' : 'http' ) . '://' . $r->hostname() . $r->unparsed_uri() ); + no strict 'subs'; + $r->status(302); + + # no kill of this child, situations have been seen where this apache child will + # never again get a good db handle + $r->child_terminate(); + + # exiting here, as returning anything will require refactoring all methods that + # call this to handles the error and bubble it up to handler() + return 'badhandle'; + } + + my $sth = $dbh->prepare($sql); + my $sql_out; + $logger->debug( "executing: $sql with " . &make_json( $parms, { allow_nonref => 1 } ) ) if ( $logger->is_debug() ); + my $rv = $sth->execute(@$parms); + $logger->error( $sth->err . " : " . $sth->errstr ) if ( $sth->err ); + if ( $sth->err ) + { + return { err => $sth->err, errstr => $sth->errstr }; + } + if ( $sql =~ /select/ ) + { + $sql_out = $sth->fetchall_arrayref( {}, undef ); + } + return { data => $sql_out }; +} + +sub recordFetch() +{ + my $requestObject = shift; + my $sql = shift; + my $parms = shift; + my $return; + + my $rtn = &doSql( $sql, $parms ); + if ($rtn eq 'badhandle') + { + $db_retry =1; + $r->headers_out->set(Location => $r->unparsed_uri() ); + no strict 'subs'; + $r->status(302); + # no kill of this child, situations have been seen where this apache child will + # never again get a good db handle + $r->child_terminate(); + return 'badhandle'; + } + if ( $$rtn{'err'} ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return $$rtn{'err'} . " : " . $$rtn{'errstr'}; + } + my $data = $$rtn{'data'}; + + # format output inside extjs compatible object if requested + if ( $$requestObject{'query'}{'_extjs'} ) + { + $return = { + records => $data, + total => scalar(@$data), + metaData => { + root => 'records', + totalProperty => 'total', + id => $tree->{entities}->{ $$requestObject{'entity'} }->{_key}, + fields => &getFieldList( $$requestObject{'entity'} ) + } + }; + if ( !scalar( @{ $$return{metaData}{fields} } ) && scalar( @{ $$return{records} } ) ) + { + @{ $$return{metaData}{fields} } = keys( %{ $$return{records}[0] } ); + } + } + elsif ( $$requestObject{'path'}[0] ) + { + $return = $$data[0]; + } + else + { + $return = $data; + } + return $return; + +} + +# handle generic updates +sub doGenericPUT +{ + my ($sql); + my $requestObject = shift; + my $entity = $$requestObject{'entity'}; + $logger->info("processing PUT"); + my $dbs = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 0 } ); + # $dbs->begin_work; # this is a noop when AutoCommit = 0 + my ( @sql, $parms, @errors ); + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + + #audit fetch record to comparison during audit + my $now = $dbh->selectcol_arrayref('select now()'); + my @entity_fields = @{ &getFieldList( $$requestObject{'entity'} ) }; + my $lkup_data = &doGenericGET($requestObject); + if ( scalar( keys(%$lkup_data) ) == 0 ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + return 'There is no resource at this location'; + + } + if ( $$lkup_data{'metaData'} ) + { + $lkup_data = $$lkup_data{'records'}[0]; + } + elsif ( ref $lkup_data eq 'ARRAYREF' ) + { + $lkup_data = $$lkup_data[0]; + } + + # strip out unchanged data and trigger mtime if needed + my $mtime; + foreach (@entity_fields) + { + $$data{$_} = &doFieldNormalization( $entity, $_, $$data{$_} ) if exists $$data{$_}; + $mtime = $$now[0] if ( exists $$data{$_} && !$tree_extended->{entities}->{'system'}->{$_}->{meta} ); + delete $$data{$_} if ( defined $$data{$_} && defined $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_} ); + } + my $blocked_changes = {}; + &runACL( $requestObject, $lkup_data, $entity, $data, $blocked_changes ); + + # if the user is not a system user, then error out now if needed + $logger->info( "changes: " . &make_json($data) ); + $logger->info( "blocked changes: " . &make_json($blocked_changes) ); + if ( $requestObject->{'user'}->{'systemuser'} ne '1' && scalar( keys(%$blocked_changes) ) ) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + return 'ACL blocked change: ' . &make_json($blocked_changes); + } + if ( scalar( keys(%$blocked_changes) ) ) + { + my $change_item = { + change_ip => $$requestObject{'ip_address'}, + change_user => $requestObject->{'user'}->{'username'}, + change_time => $$now[0], + entity => $$requestObject{'entity'}, + entity_key => $$lkup_data{ $tree->{'entities'}->{ $$requestObject{'entity'} }->{'_key'} }, + change_content => &make_json($blocked_changes) + }; + &doGenericPOST( + { + entity => 'change_queue', + body => &make_json($change_item), + } + ); + $logger->info("queued change"); + return "Change queued for approval"; + } + + foreach my $f (@entity_fields) + { + if ( exists $$data{$f} ) + { + if ( $$data{$f} eq '' ) + { + push( @$parms, undef ); + } + else + { + push( @$parms, $$data{$f} ); + } + + push( @sql, "$f=?" ); + + #audit check each field and record change if done + if ( !defined $$lkup_data{$f} || $$data{$f} ne $$lkup_data{$f} ) + { + $dbs->do( + 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=?, + change_user=?, + change_ip=?', + {}, + ( + $entity, + $$lkup_data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} }, + $f, #field + $$lkup_data{$f}, #old val + $$data{$f}, # new val + $$now[0], + $requestObject->{'user'}->{'username'}, # user + $$requestObject{'ip_address'} # ip + ) + ); + } + } + } + if ( scalar(@sql) == 0 ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NO_CONTENT; + $dbs->commit; + return; + } + my $sql_set = join( ',', @sql ); + + # assemple final sql + $sql = "update $entity set $sql_set where " . $tree->{entities}->{$entity}->{_key} . "=?"; + push( @$parms, $$requestObject{'path'}[0] ); + + ## do sql and record any errors + my $sth = $dbs->prepare($sql); + if ( $sth->err ) + { + push( @errors, $dbs->err . ": " . $dbs->errstr ); + $logger->error( $sth->err . " : " . $sth->errstr ); + } + $logger->debug( "executing: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + my $rv = $sth->execute(@$parms); + if ( $sth->err ) + { + push( @errors, $dbs->err . ": " . $dbs->errstr ); + $logger->error( $sth->err . " : " . $sth->errstr ); + } + + if ( scalar(@errors) ) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return \@errors; + } + else + { + $dbs->commit; + + # check to see if key value was chnaged during this put, and adjust for GET + if ( $$data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} } ) + { + $$requestObject{'path'}[0] = $$data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} }; + } + return &doGenericGET($requestObject); + } + +} + +# handle generic creations +sub doGenericPOST +{ + my ($sql); + my $requestObject = shift; + my $entity = $$requestObject{'entity'}; + $logger->info("processing POST"); + my ( @sql, $parms ); + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + my $blocked_changes = {}; + &runACL( $requestObject, {}, $entity, $data, $blocked_changes ); + $logger->info( "blocked POST fields: " . &make_json($blocked_changes) ); + + my $now = $dbh->selectcol_arrayref('select now()'); + # if the user is not a system user, then error out now if needed + if ( $requestObject->{'user'}->{'systemuser'} ne '1' && scalar( keys(%$blocked_changes) ) ) + { + $dbh->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + return 'ACL blocked change: ' . &make_json($blocked_changes); + } + $data = &applyDefaults($data,$entity); + foreach my $f ( @{ &getFieldList( $$requestObject{'entity'} ) } ) + { + if ( exists $$data{$f} ) + { + $$data{$f} = &doFieldNormalization( $entity, $f, $$data{$f} ); + push( @$parms, $$data{$f} ); + push( @sql, "$f=?" ); + } + } + my $sql_set = join( ',', @sql ); + $sql = "insert into $entity set $sql_set"; + my $sth = $dbh->prepare($sql); + + # set for error and return if db prepare had errors + $logger->error( $sth->err . " : " . $sth->errstr ) if ( $sth->err ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR if $sth->err; + return $sth->err . " : " . $sth->errstr if ( $sth->err ); + + #audit entry for create + $dbh->do( + 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=?, + change_user=?, + change_ip=?', + {}, + ( + $entity, + $$data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} }, + 'record', #field + '', #old val + 'CREATED', # new val + $$now[0], + $requestObject->{'user'}->{'username'}, # user + $$requestObject{'ip_address'} # ip + ) + ); + + # run sql + $logger->debug( "executing: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + my $rv = $sth->execute(@$parms); + + #return error if db insert had errors + if ( $sth->err ) + { + $logger->error( $sth->err . " : " . $sth->errstr ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return $sth->err . " : " . $sth->errstr; + } + $$requestObject{'headers_out'} = [ 'Location', "/cmdb_api/v1/" . $entity . "/" . $$data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} } ]; + return; +} + +sub doAclDELETE +{ + doGenericDELETE(@_); +} + +sub doAclGET +{ + doGenericGET(@_); +} + +sub doAclPOST +{ + my ($sql); + my $requestObject = shift; + my $entity = $$requestObject{'entity'}; + $logger->info("processing POST"); + my ( @sql, $parms ); + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + + if ( $$data{'logic'} ) + { + my $r = {}; + my $f = ''; + my $req = $requestObject; + my $changes = {}; + eval( $$data{'logic'} ); + if ($@) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return "Syntax error in ACL logic: $@"; + } + } + + foreach my $f ( @{ &getFieldList( $$requestObject{'entity'} ) } ) + { + if ( exists $$data{$f} ) + { + $$data{$f} = &doFieldNormalization( $entity, $f, $$data{$f} ); + push( @$parms, $$data{$f} ); + push( @sql, "$f=?" ); + } + } + my $sql_set = join( ',', @sql ); + $sql = "insert into $entity set $sql_set"; + my $sth = $dbh->prepare($sql); + if ( $sth->err ) + { + $logger->error( $sth->err . " : " . $sth->errstr ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return $sth->err . " : " . $sth->errstr; + } + $logger->debug( "executing: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + my $rv = $sth->execute(@$parms); + if ( $sth->err ) + { + $logger->error( $sth->err . " : " . $sth->errstr ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return $sth->err . " : " . $sth->errstr; + } + $$requestObject{'headers_out'} = [ 'Location', "/cmdb_api/v1/acl/" . $$data{'acl_id'} ]; + return; + +} + +sub doAclPUT +{ + my ($sql); + my $requestObject = shift; + my $entity = $$requestObject{'entity'}; + $logger->info("processing PUT"); + my $dbs = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 1 } ); + $dbs->begin_work; + my ( @sql, $parms, @errors ); + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + + #audit fetch record to comparison during audit + my $now = $dbh->selectcol_arrayref('select now()'); + my @entity_fields = @{ &getFieldList( $$requestObject{'entity'} ) }; + my $lkup_data = &doGenericGET($requestObject); + if ( $$lkup_data{'metaData'} ) + { + $lkup_data = $$lkup_data{'records'}[0]; + } + elsif ( ref $lkup_data eq 'ARRAYREF' ) + { + $lkup_data = $$lkup_data[0]; + } + + # strip out unchanged data and trigger mtime if needed + my $mtime; + foreach (@entity_fields) + { + $$data{$_} = &doFieldNormalization( $entity, $_, $$data{$_} ) if exists $$data{$_}; + $mtime = $$now[0] if ( exists $$data{$_} && !$tree_extended->{entities}->{'system'}->{$_}->{meta} ); + delete $$data{$_} if ( exists $$data{$_} && exists $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_} ); + } + my $blocked_changes = {}; + &runACL( $requestObject, $lkup_data, $entity, $data, $blocked_changes ); + + # If the change is otherwise acceptable, and we are changing the logic, + # verify syntax + if ( $$data{'logic'} ) + { + my $r = {}; + my $f = ''; + my $req = $requestObject; + my $changes = {}; + eval( $$data{'logic'} ); + if ($@) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return "Syntax error in ACL logic: $@"; + } + } + + # if the user is not a system user, then error out now if needed + $logger->info( "changes: " . &make_json($data) ); + $logger->info( "blocked changes: " . &make_json($blocked_changes) ); + if ( $requestObject->{'user'}->{'systemuser'} ne '1' && scalar( keys(%$blocked_changes) ) ) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return 'ACL blocked change: ' . &make_json($blocked_changes); + } + if ( scalar( keys(%$blocked_changes) ) ) + { + my $change_item = { + change_ip => $$requestObject{'ip_address'}, + change_user => $requestObject->{'user'}->{'username'}, + change_time => $$now[0], + entity => $$requestObject{'entity'}, + entity_key => $$lkup_data{ $tree->{'entities'}->{ $$requestObject{'entity'} }->{'_key'} }, + change_content => &make_json($blocked_changes) + }; + &doGenericPOST( + { + entity => 'change_queue', + body => &make_json($change_item), + } + ); + $logger->info("queued change"); + return "Change queued for approval"; + } + + foreach my $f (@entity_fields) + { + if ( exists $$data{$f} ) + { + if ( $$data{$f} eq '' ) + { + push( @$parms, undef ); + } + else + { + push( @$parms, $$data{$f} ); + } + + push( @sql, "$f=?" ); + + #audit check each field and record change if done + if ( $$data{$f} ne $$lkup_data{$f} ) + { + $dbs->do( + 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=?, + change_user=?, + change_ip=?', + {}, + ( + $entity, + $$lkup_data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} }, + $f, #field + $$lkup_data{$f}, #old val + $$data{$f}, # new val + $$now[0], + $requestObject->{'user'}->{'username'}, # user + $$requestObject{'ip_address'} # ip + ) + ); + } + } + } + my $sql_set = join( ',', @sql ); + + # assemple final sql + $sql = "update $entity set $sql_set where " . $tree->{entities}->{$entity}->{_key} . "=?"; + push( @$parms, $$requestObject{'path'}[0] ); + + ## do sql and record any errors + my $sth = $dbs->prepare($sql); + if ( $sth->err ) + { + push( @errors, $dbs->err . ": " . $dbs->errstr ); + $logger->error( $sth->err . " : " . $sth->errstr ); + } + $logger->debug( "executing: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + my $rv = $sth->execute(@$parms); + if ( $sth->err ) + { + push( @errors, $dbs->err . ": " . $dbs->errstr ); + $logger->error( $sth->err . " : " . $sth->errstr ); + } + + if ( scalar(@errors) ) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return \@errors; + } + else + { + $dbs->commit; + } + return; +} + +sub doChangeQueuePOST() +{ + doGenericPOST(@_); +} + +sub doChangeQueuePUT() +{ + doGenericPUT(@_); +} + +sub doChangeQueueDELETE +{ + doGenericDELETE(@_); +} + +#handles changequeue fetches +sub doChangeQueueGET() +{ + # for internal requests: + # ro = { entity, path[0] query } + + my ( $keyval, $sql, $parms, $return ); + $logger->info("processing GET"); + my $requestObject = shift; + + # check to see if this entity requires special processing, otherwise handle with generic + # assemble sql based on input parameters + $sql = "select * from change_queue ch left join device d on ch.entity_key=d.fqdn where "; + + # check for path key value and add if specified + if ( $$requestObject{'path'}[0] ) + { + $logger->debug("found $tree->{entities}->{$$requestObject{'entity'}}->{_key} : $$requestObject{'path'}[0] in url") if ( $logger->is_debug() ); + $sql .= " $tree->{entities}->{$$requestObject{'entity'}}->{_key} like ?"; + push( @$parms, $$requestObject{'path'}[0] ); + } + $logger->debug("getparms: $$requestObject{getparams}") if ( $logger->is_debug() ); + my $device_fields = &getFieldList( 'device', 0 ); + my $change_fields = &getFieldList( 'change_queue', 0 ); + if ( $$requestObject{getparams} ) + { + my @ranges = split( /[&;]/, $$requestObject{getparams} ); + foreach my $range (@ranges) + { + next unless $range =~ /(\w+)([!~>=<]+)(.+)/; + my $key = $1; + my $op = $2; + my $val = $3; + next if $key =~ /^_/; + next unless ( grep( /^$key$/, @$device_fields ) || grep( /^$key$/, @$change_fields ) ); + $val =~ s/'/%/g; + $op = 'LIKE' if $op eq '='; + $op = 'NOT LIKE' if $op eq '!='; + $op = 'RLIKE' if $op eq '~'; + $op = 'NOT RLIKE' if $op eq '!~'; + $logger->debug("Found param: $key $op $val") if ( $logger->is_debug() ); + $val =~ s/\*/%/g; + $sql .= " and " if ( $sql !~ /where\ $/ ); + $sql .= grep( /^$key$/, @$device_fields ) ? " d." : " ch."; + $sql .= "$key $op '$val'"; + + } + } + $sql =~ s/where\ // if ( $sql =~ /where\ $/ ); + my $rtn = &recordFetch( $requestObject, $sql, $parms ); + + if ( ref $rtn eq 'HASH' && defined $rtn->{metaData} && defined $rtn->{metaData}->{fields} ) + { + push( @{ $rtn->{metaData}->{fields} }, @$device_fields ); + } + return $rtn; +} + +#handles generic fetches +sub doGenericGET() +{ + # for internal requests: + # ro = { entity, path[0] query } + + my ( $keyval, $sql, $parms, $return ); + $logger->info("processing GET"); + my $requestObject = shift; + + # assemble sql based on input parameters + $sql = "select " . join(',',@{ &getFieldList( $$requestObject{'entity'} ) }). " from $$requestObject{'entity'} where "; + + # check for path key value and add if specified + if ( $$requestObject{'path'}[0] ) + { + $logger->debug("found $tree->{entities}->{$$requestObject{'entity'}}->{_key} : $$requestObject{'path'}[0] in url") if ( $logger->is_debug() ); + $sql .= " $tree->{entities}->{$$requestObject{'entity'}}->{_key} like ?"; + push( @$parms, $$requestObject{'path'}[0] ); + } + elsif ( $$requestObject{getparams} ) + { + my @ranges = split( /[&;]/, $$requestObject{getparams} ); + foreach my $range (@ranges) + { + next unless $range =~ /(\w+)([!~>=<]+)(.+)/; + my $key = $1; + my $op = $2; + my $val = $3; + next if $key =~ /^_/; + $val =~ s/'//g; + $op = 'LIKE' if $op eq '='; + $op = 'NOT LIKE' if $op eq '!='; + $op = 'RLIKE' if $op eq '~'; + $op = 'NOT RLIKE' if $op eq '!~'; + $logger->debug("Found param: $key $op $val") if ( $logger->is_debug() ); + $val =~ s/\*/%/g; + $sql .= " and " if ( $sql !~ /where\ $/ ); + $sql .= " $key $op '$val'"; + + } + } + $sql =~ s/where\ // if ( $sql =~ /where\ $/ ); + my $rec = &recordFetch( $requestObject, $sql, $parms ); + if ( !$rec ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + return; + } + else + { + return $rec; + } +} + +sub doGenericDELETE +{ + my $requestObject = shift; + my $entity = $$requestObject{'entity'}; + my ( $sql, $parms ); + + # continue if entity key segment of the path is there + if ( $requestObject->{'path'}[0] ) + { + my $lkup_data = &doGenericGET($requestObject); + if ( !defined $lkup_data ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + return; + } + if ( $$lkup_data{'metaData'} ) + { + $lkup_data = $$lkup_data{'records'}[0]; + } + elsif ( ref $lkup_data eq 'ARRAYREF' ) + { + $lkup_data = $$lkup_data[0]; + } + my $blocked_changes = {}; + &runACL( $requestObject, $lkup_data, $entity, {}, $blocked_changes ); + + # if the user is not a system user, then error out now if needed + $logger->info( "blocked DELETE operation: " . &make_json($blocked_changes) ); + if ( $requestObject->{'user'}->{'systemuser'} ne '1' && scalar( keys(%$blocked_changes) ) ) + { + $dbh->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + return 'ACL blocked Delete: ' . &make_json($blocked_changes); + } + + $sql = "delete from $requestObject->{'entity'} where $tree_extended->{entities}->{ $requestObject->{'entity'} }->{'_key'} = ?"; + $parms = [ $requestObject->{'path'}[0] ]; + $dbh->do( $sql, {}, @$parms ); + if ( $dbh->err ) + { + $logger->error( "sql: $sql with " . join( ',', @$parms ) ); + $logger->error( $dbh->err . " : " . $dbh->errstr ) if ( $dbh->err ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + } + else + { + $dbh->do( + 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=now(), + change_user=?, + change_ip=?', + {}, + ( + $requestObject->{'entity'}, + $requestObject->{'path'}[0], + 'record', #field + substr( make_json($lkup_data), 0, 100 ) . '...', #old val + 'DELETED', # new val + $requestObject->{'user'}->{'username'}, # user + $requestObject->{'ip_address'} # ip + ) + ); + } + + } + +} + +sub parseQueryParams +{ + my ( $data, $getparams, $valid_fields ) = @_; + + my @ranges = split( /[&;]/, $data ); + foreach my $range (@ranges) + { + # next unless $range =~ /(\w+)([!~>=<]+)(.+)/; + next unless $range =~ /(\w+)([!~>=<]+)(.*)/; + my $key = $1; + my $op = $2; + my $val = $3; + next if $key =~ /^_/; + next if ( !grep( /^$key$/, @$valid_fields ) && scalar(@$valid_fields) > 0 ); + $val =~ s/'//g; + $op = 'LIKE' if $op eq '='; + $op = 'NOT LIKE' if $op eq '!='; + $op = 'RLIKE' if $op eq '~'; + $op = 'NOT RLIKE' if $op eq '!~'; + $logger->debug("Found param: $key $op $val") if ( $logger->is_debug() ); + $$getparams{$key}{op} = $op; + $$getparams{$key}{val} = $val; + } + + return $getparams; +} + +sub doEnvironmentsServicesGET() +{ + my $requestObject = shift; + my $environment; + my $service; + my @parents; + my @params; + my %getparams; + my %hash; + my $environment_tag; + + $environment = $requestObject->{'path'}[0]; + $service = $requestObject->{'path'}[2]; + + my $sql = "select name, environment_name from environments"; + + my $rtn = &doSql( $sql, undef ); + if ( $$rtn{'err'} ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return $$rtn{'err'} . " : " . $$rtn{'errstr'}; + } + + if ( $requestObject->{'getparams'} ) + { + @params = split( /[&;]/, $requestObject->{'getparams'} ); + parseQueryParams( $requestObject->{'getparams'}, \%getparams, [ 'type', 'name' ] ); + } + + $environment_tag = 1 if (defined $requestObject->{'query'}->{'_tag_environment'} || defined $requestObject->{'query'}->{'_meta'}); + + for my $env ( @{ $rtn->{'data'} } ) + { + my $parent = $env->{'environment_name'}; + my $name = $env->{'name'}; + + if ( $parent eq $name ) + { + $parent = undef; + } + + $hash{$name} = $parent; + } + + $parents[0] = $environment; + while ( defined $parents[-1] ) + { + push @parents, $hash{ $parents[-1] }; + } + pop @parents; + %hash = (); + my $list = join( ', ', map { "'$_'" } reverse(@parents) ); + $sql = "select name, environment_name, note, s.svc_id, type, data_key, data_value from " . " (select name, environment_name, note, svc_id, type from service_instance " . " where name like '%' "; + + if ( defined $service ) + { + $sql .= "and name like '$service' "; + } + + for my $key ( keys %getparams ) + { + $sql .= sprintf "and %s %s '%s' ", $key, $getparams{$key}{op}, $getparams{$key}{val}; + } + + $sql .= "and environment_name in ($list)) as s " . "left join service_instance_data as d on s.svc_id = d.svc_id " . "order by field(environment_name, $list)"; + $rtn = &doSql( $sql, undef ); + + if ( $$rtn{'err'} ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return $$rtn{'err'} . " : " . $$rtn{'errstr'}; + } + + for my $data ( @{ $rtn->{'data'} } ) + { + if ( not exists $hash{ $data->{'name'} } ) + { + $hash{ $data->{'name'} } = { + name => $data->{'name'}, + environment_name => $data->{'environment_name'}, + type => $data->{'type'}, + svc_id => $data->{'svc_id'}, + note => $data->{'note'}, + }; + } + else + { + $hash{ $data->{'name'} }->{'name'} = $data->{'name'}; + $hash{ $data->{'name'} }->{'environment_name'} = $data->{'environment_name'}; + $hash{ $data->{'name'} }->{'type'} = $data->{'type'}; + $hash{ $data->{'name'} }->{'svc_id'} = $data->{'svc_id'}; + $hash{ $data->{'name'} }->{'note'} = $data->{'note'}; + } + + my $svc = $hash{ $data->{'name'} }; + my $key = $data->{'data_key'}; + my $value = $data->{'data_value'}; + next if ( not defined $key ); + next if ( grep(/^$key$/,@{&getFieldList('service_instance')}) ); + # next if ( ( not defined $key ) || exists $svc->{$key} ); + + if ($environment_tag) + { + my $inherited=0; + if(exists $svc->{$key}) + { + $inherited = $svc->{$key}; + $logger->debug("inherited $key"); + } + + $svc->{$key} = { + value => $value, + environment_name => $data->{'environment_name'} + }; + + if($inherited != 0) + { + $svc->{$key}->{'inherited'} = $inherited; + } + + } + else + { + $svc->{$key} = $value; + } + } + + if ( $requestObject->{'getparams'} ) + { + parseQueryParams( $requestObject->{'getparams'}, \%getparams, [] ); + } + + # remove params that would have gotten parsed into the sql query + foreach my $query_parm ( [ 'name', 'type' ] ) + { + delete $getparams{$query_parm}; + } + + # if other attribute querys are coming in, then evaluate them assemble new results of matches + if ( scalar( keys(%getparams) ) > 0 ) + { + my @ranges = split( /[&;]/, $requestObject->{'getparams'} ); + my $jcel_params = {}; + foreach my $range (@ranges) + { + next unless $range =~ /(\w+)([!~>=<]+)(.*)/; + my $key = $1; + my $op = $2; + my $val = $3; + + # we are parsing the query string (yet again). skip the attrs that get searched by the sql + next if ( grep( /^$key$/, [ 'name', 'type' ] ) ); + $jcel_params->{$key}->{$op} = $val; + } + my @filtered_results; + + # assemble jcel config from passed in parameters to use for testing the service records + $logger->debug( "jcel condition: " . make_json($jcel_params) ); + my $jcel = NOMS::JCEL->new($jcel_params); + foreach my $service_record_key ( keys %hash ) + { + if ( $jcel->test( $hash{$service_record_key} ) ) + { + push( @filtered_results, \%{ $hash{$service_record_key} } ); + } + } + return \@filtered_results if scalar(@filtered_results); + + # there were no results left after filtering. so + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + return; + + } + + # return results as array when query is used + if ( !defined $service && keys(%hash) ) + { + return [ values %hash ]; + } + + # otherwise service was accessed via full resource path, so return as record + elsif ( keys %hash ) + { + return ( values %hash )[0]; + } + else + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + return; + } +} + +sub insertAuditEntry +{ + my ( $dbh, $requestObject, $entity, $key, $name, $old, $new, $time ) = @_; + + my $sql = 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=?, + change_user=?, + change_ip=?'; + + $dbh->do( $sql, {}, ( $entity, $key, $name, $old, $new, $time, $requestObject->{user}->{username}, $$requestObject{ip_address} ) ); +} + +sub executeDbStatement +{ + my ( $sth, $sql, @parms ) = @_; + + if ( $logger->is_debug() ) + { + my $msg; + + if (@parms) + { + $msg = "executing: $sql with " . join( ',', @parms ); + } + else + { + $msg = "executing: $sql"; + } + + $logger->debug($msg); + } + + return $sth->execute(@parms); +} + +sub doEnvironmentsServicesPUT() +{ + my $requestObject = shift; + $logger->info("processing PUT"); + my $dbh = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 0, RaiseError => 1 } ); + my $environment = $requestObject->{'path'}[0]; + my $service = $requestObject->{'path'}[2]; + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + my $blocked_changes = {}; + my $svc_id; + my $sth; + my $lkup_data; + my @inserts; + my @updates; + my @deletes; + my %service_updates; + my %service_attributes; + my $sql; + my $error; + my $old_value; + my $new_value; + my $did_update; + + eval { + # Get service_instance record for the requested service + $sql = "select svc_id,type,name,environment_name,note from service_instance where environment_name=? and name=?"; + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, $environment, $service ); + $lkup_data = $sth->fetchall_arrayref( {}, undef ); + + if ( $sth->rows == 0 ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + $dbh->rollback; + $error = 'There is no resource at this location'; + return; + } + elsif ( $sth->rows > 1 ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + $dbh->rollback; + $error = 'Multiple services with the same ID'; + return; + } + + $old_value = &doEnvironmentsServicesGET($requestObject); + + &runACL( $requestObject, {}, 'services', $data, $blocked_changes ); + + # if the user is not a system user, then error out now if needed + $logger->info( "blocked PUT fields: " . &make_json($blocked_changes) ); + my $now = $dbh->selectcol_arrayref('select now()'); + if ( $requestObject->{'user'}->{'systemuser'} ne '1' + && scalar( keys(%$blocked_changes) ) ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + $dbh->rollback; + $error = 'ACL blocked change: ' . &make_json($blocked_changes); + return; + } + + $lkup_data = $$lkup_data[0]; + $svc_id = $lkup_data->{'svc_id'}; + + # Determine if we need to modify any fields of the service instance record + + my $fieldlist = &getFieldList('service_instance'); + for my $f ( @{$fieldlist} ) + { + my $value = $lkup_data->{$f}; + if ( defined $data->{$f} && $value ne $data->{$f} ) + { + $service_updates{$f} = [ $value, $data->{$f} ]; + } + + delete $data->{$f}; #ICK + } + delete $data->{'svc_id'}; #ICK + + # Get all service_instance_data records that belong to this instance + # We don't care about inheritance for this + $sql = "select data_id,data_key,data_value " . "from service_instance_data where svc_id=?"; + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, $svc_id ); + $lkup_data = $sth->fetchall_arrayref( {}, undef ); + + #populate lookup data into a structure of the service_instance for reference + for my $row (@$lkup_data) + { + $service_attributes{ $row->{'data_key'} } = + [ $row->{'data_value'}, $row->{'data_id'} ]; + } + + my $field_list = &getFieldList('service_instance'); + for my $key ( keys %$data ) + { + #skip if a native service_instance field + if ( grep( /^$key$/, @$field_list ) ) + { + next; + } + if ( exists $service_attributes{$key} ) + { + if ( not defined $data->{$key} ) + { + push @deletes, $key; + } + elsif ( $service_attributes{$key}[0] ne $data->{$key} ) + { + push @updates, $key; + } + } + else + { + push @inserts, $key; + } + } -sub eat_json { - my ($json_text, $opthash) = @_; - return ($JSON::VERSION > 2.0 ? from_json($json_text, $opthash) : JSON->new()->jsonToObj($json_text, $opthash)); -} + if ( keys %service_updates ) + { + my $sql_set; + my @parms; + + $sql_set = join( ', ', map { "$_=?" } keys %service_updates ); + for my $key ( keys %service_updates ) + { + push @parms, $service_updates{$key}[1]; + } + push @parms, $svc_id; + + $sql = "update service_instance set $sql_set"; + $sql .= " where svc_id=?"; + + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, @parms ); + $did_update = 1; + } -sub make_json { - my ($obj, $opthash) = @_; - return ($JSON::VERSION > 2.0 ? to_json($obj, $opthash) : JSON->new()->objToJson($obj, $opthash)); -} + if (@inserts) + { + # Create any new service_instance_data records + $sql = "insert into service_instance_data " . "(data_key, data_value, svc_id) values "; + $sql .= join( ',', map { sprintf( "('%s','%s','%s')", $_, $data->{$_}, $svc_id ) } @inserts ); + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql ); + $did_update = 1; + } -my $opt = Optconfig->new('cmdb_api', { 'driver=s' => 'mysql', - 'dbuser=s' => 'dbuser', - 'dbpass=s' => 'dbpass', - 'dbhost' => 'localhost', - 'database' => 'inventory', - 'debug' => 1, - 'prism_domain' => 'prism.ppops.net', - 'logconfig' => '/var/www/cmdb_api/log4perl.conf', - 'lexicon' => '/var/www/cmdb_api/pp_lexicon.xml', - 'entities' => { - acl=>'Acl', - vip=>'Generic', - datacenter_subnet=>'Generic', - data_center=>'Generic', - role=>'Generic', - pod_cluster=>'Generic', - snat=>'Generic', - pool=>'Generic', - cluster=>'Generic', - hardware_model=>'Generic', - cluster_mta=>'Generic', - system=>'System', - device=>'System', - blade_chassis=>'System', - console_server=>'System', - firewall=>'System', - load_balancer=>'System', - network_switch=>'System', - power_strip=>'System', - router=>'System', - storage_head=>'System', - storage_shelf=>'System', - device_ip=>'Generic', - newhostname=>'Provision', - pcmsystemname=>'ProvisionPcm', - user=>'Generic', - currentUser=>'User', - inv_audit=>'Generic', - audit=>'Audit', - inv_normalizer=>'Generic', - fact=>'TrafficControl', - change_queue=>'ChangeQueue', - ip=>'Generic', - service_instance=>'Generic', - service_instance_data=>'Generic', - instance_size=>'Generic', - instance_location=>'Generic', - column_lkup=>'Column_lkup', - environments=>'Environments' - } - }); - -my $valid_entity_apis={ - 'Environments' => 1, - 'ChangeQueue' => 1, - 'System' => 1, - 'Generic' => 1, - 'Audit' => 1 -}; + if (@updates) + { + # Modify existing service_instance_data records + $sql = "update service_instance_data set " . "data_value=? where data_key=? and svc_id=?"; + $sth = $dbh->prepare($sql); + + for my $key (@updates) + { + executeDbStatement( $sth, $sql, $data->{$key}, $key, $svc_id ); + } + $did_update = 1; + } -##TODO unhardcode this crap -my @system_native_fields=qw(fqdn inventory_component_type system_type status ip_address - mac_address data_center_code cage_code rack_code rack_position manufacturer product_name - serial_number agent_reported); -# my @system_meta_fields=qw(asset_tag_number disk_drive_count file_systems physical_processor_count processors memory_size -# virtual operating_system operating_system_release primary_interface interfaces drac roles customers power_supply_count -# power_supply_watts is_virtual guest_fqdns host_fqdn); - -my $DEBUG=$opt->{'debug'}; -my $DBHOST=$opt->{'dbhost'}; -my $DBUSER=$opt->{'dbuser'}; -my $DBPASS=$opt->{'dbpass'}; -my $DATABASE=$opt->{'database'}; -my $DRIVER=$opt->{'driver'}; -my ($lexicon,$tree,$parser); -my ($parms); -my $log_config_file=$opt->{'logconfig'}; + if (@deletes) + { + $sql = "delete from service_instance_data where " . "svc_id=? and data_key in "; + $sql .= '(' . join( ',', map { "'$_'" } @deletes ) . ')'; + $sth = $dbh->prepare($sql); -Log::Log4perl::init($log_config_file); + executeDbStatement( $sth, $sql, $svc_id ); + $did_update = 1; + } -my $logger = Log::Log4perl->get_logger('inventory.cmdb_api'); -unless($lexicon) -{ -#TODO this hardcoded path is bad fix it - $lexicon=$opt->{'lexicon'}; -} + $dbh->commit; -# database connection -#my $dbh=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS); -our $dbh; -#valid api types. these must exist and be parsable in the lexicon if they are 'Generic' -# or have provided doGET/PUT/POST functions + #adjust key path, if key field value was changed + if ( $service_updates{'name'} ) + { + $logger->debug( make_json($requestObject) ); + $requestObject->{'path'}->[2] = $service_updates{'name'}[1]; + $logger->debug( make_json($requestObject) ); + } -my $valid_entities = $opt->{'entities'}; + $new_value = &doEnvironmentsServicesGET($requestObject); + if ($did_update) + { + insertAuditEntry( $dbh, $requestObject, 'services', "$environment/$service", 'record', make_json($old_value), make_json($new_value), $$now[0] ); + $dbh->commit; + } + }; + if ($@) + { + my $errstr; -# my $valid_entities={ -# acl=>'Acl', -# vip=>'Generic', -# datacenter_subnet=>'Generic', -# role=>'Generic', -# pod_cluster=>'Generic', -# snat=>'Generic', -# pool=>'Generic', -# cluster=>'Generic', -# hardware_model=>'Generic', -# cluster_mta=>'Generic', -# system=>'System', -# device=>'System', -# blade_chassis=>'System', -# console_server=>'System', -# firewall=>'System', -# load_balancer=>'System', -# network_switch=>'System', -# power_strip=>'System', -# router=>'System', -# storage_head=>'System', -# storage_shelf=>'System', -# device_ip=>'Generic', -# newhostname=>'Provision', -# pcmsystemname=>'ProvisionPcm', -# user=>'Generic', -# currentUser=>'User', -# inv_audit=>'Generic', -# audit=>'Audit', -# inv_normalizer=>'Generic', -# fact=>'TrafficControl', -# change_queue=>'ChangeQueue', -# ip=>'Generic', -# service_instance=>'Generic', -# service_instance_data=>'Generic', -# instance_size=>'Generic', -# instance_location=>'Generic' -# }; - -my $versions=[ 'v1' ]; - -$parser= XML::Simple->new( ); -eval { $tree=$parser->XMLin($lexicon); }; -# show error and die if xml parsing of the lexicon failed -if($@) -{ - $logger->fatal("error parsing $lexicon\n$@"); - exit; -} + if ( defined $sth && $sth->err ) + { + $errstr = $sth->err . " : " . $sth->errstr; + $logger->error($errstr); + } + else + { + $errstr = $@; + } -$logger->info("$lexicon is xml ok"); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; -my $tree_extended; -$tree_extended=&eat_json(&make_json($tree)); -# loop through entities and add base attributes to things that subclass other stuff -foreach(keys(%{$tree_extended->{entities}})) -{ - if($tree_extended->{entities}->{$_}->{extends}) - { - my $extends=&lkupXMLPath($tree->{entities}->{$_}->{extends}); - foreach my $attr (keys(%$extends)) - { - $tree_extended->{entities}->{$_}->{$attr}=$extends->{$attr}; - } - } -} -$logger->debug("lexicon: " . &make_json($tree,{pretty=>1,allow_nonref=>1}) ) if ($logger->is_debug()); -$logger->debug("lexicon extended: " . &make_json($tree_extended,{pretty=>1,allow_nonref=>1}) ) if ($logger->is_debug()); + eval { $dbh->rollback; }; -sub lkupXMLPath() -{ - my $str=shift; - my @seg=split('/',$str); - shift(@seg); - shift(@seg); - my $rtn='$tree->{' . shift(@seg) . '}'; - foreach(@seg) - { - $rtn.='->{' . $_ . '}'; - } - return eval($rtn); -} + $error = $errstr; + } -# mod perl2 handler -sub handler() { - $dbh=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS); - my $r = shift; - my $up_uri = $r->unparsed_uri(); - $up_uri =~ s/.+\?//; - my $uri = uri_unescape($up_uri); - my $req=Apache2::Request->new($r); - my ($requestObject,$data,$formatted_data); - %{$$requestObject{'query'}}=%{$req->param} if $req->param; - $$requestObject{'getparams'}=$uri; - $$requestObject{'stat'}=Apache2::Const::HTTP_OK; - $$requestObject{'_format'}=$$requestObject{'query'}{'_format'} || 'json'; - $$requestObject{'method'}=$req->method(); - @{$$requestObject{'path'}}=split('/',$req->uri()); - $$requestObject{'pathstr'}=$req->uri(); - $$requestObject{'user'}=&doGenericGET({entity=>'user',path=>[$req->user]}) if $req->user; - $$requestObject{'http_auth_user'}=$req->user if $req->user; - $$requestObject{'ip_address'}=$r->connection->remote_ip(); - if($$requestObject{'method'} ne 'GET') - { - $$requestObject{'body'}= read_post($r); - } - else - { - $$requestObject{'body'}=''; - } - shift(@{$$requestObject{'path'}}); - if(shift(@{$$requestObject{'path'}}) eq 'cmdb_api') - { - $$requestObject{'requested_api'}=shift(@{$$requestObject{'path'}}); - $$requestObject{'entity'}=shift(@{$$requestObject{'path'}}); - $logger->debug(&make_json($requestObject,{pretty=>1,allow_nonref=>1,allow_blessed=>1})) if ($logger->is_debug()); - # do help if it was asked - if( exists $requestObject->{'query'}->{'help'} || exists $requestObject->{'query'}->{'lexicon'}) - { - $requestObject->{'help'}=1; - if(!$requestObject->{'requested_api'}) - { - $r->print(&make_json($versions)); - return Apache2::Const::OK; - - } - elsif( !$requestObject->{'entity'}) - { - my $ents=[]; - my $lex={}; - if($requestObject->{'query'}->{'lexicon'}) - { - foreach(keys(%$valid_entities)) - { - $lex->{$_}= $tree->{entities}->{$_}; - no strict 'refs'; - # check each attribute and populate enumerations if needed - foreach my $attr (keys(%{$lex->{$_}})) - { - if($lex->{$_}->{$attr} && ref($lex->{$_}->{$attr}) eq 'HASH' && - defined $lex->{$_}->{$attr}->{'enumeration'} && - defined $lex->{$_}->{$attr}->{'enumeration'}->{'entity'} && - defined $lex->{$_}->{$attr}->{'enumeration'}->{'attribute'}) - { - $lex->{$_}->{$attr}->{'enumeration'}->{'enumerator'}=&doColumn_lkupGET($requestObject,$lex->{$_}{$attr}{'enumeration'}{'entity'},$lex->{$_}{$attr}{'enumeration'}->{'attribute'}); - } - } - } - $r->print(&make_json($lex)); - } - else - { - foreach(keys(%$valid_entities)) - { - push(@$ents,$_) if ( $valid_entity_apis->{ $valid_entities->{$_} } == 1 ); - } - $r->print(&make_json($ents)); - } - return Apache2::Const::OK; - } - else - { - #$r->print(&make_json(&getFieldList($requestObject->{'entity'}))); - $r->print(&make_json( $tree->{entities}->{$requestObject->{'entity'}}, {pretty => 1,allow_nonref=>1})); - return Apache2::Const::OK; - } - - } - - - # check for valid entity - unless($$requestObject{'entity'} && $$valid_entities{$$requestObject{'entity'}}) - { - $logger->debug( "valid entities:") if ($logger->is_debug()); - $logger->debug( "entity lkup: $$valid_entities{$$requestObject{'entity'}}") if ($logger->is_debug()); - $r->print('valid entity required'); - return Apache2::Const::HTTP_NOT_ACCEPTABLE; - } - - #deal with the connection and produce data - $data=&ProcessRequest($requestObject); - $r->status($$requestObject{'stat'}); - if($$requestObject{'stat'} eq '500') - { - $data={ - success => 'false', - message => $data - }; - } - $logger->debug( "final return of status: $$requestObject{'stat'}") if ($logger->is_debug()); - -#TODO reconcile the '"string" data that comes back from above and how we output it (errors, etc...) - if($$requestObject{'headers_out'}) - { - $r->headers_out->add($$requestObject{'headers_out'}[0]=>$$requestObject{'headers_out'}[1]); - } -#TODO make output format based on accept content header - if(!defined $data && keys(%{$$requestObject{'query'}}) > 0 ) - { - $data = []; - } - # set output format - if(defined $data) - { - if($$requestObject{'_format'} eq 'json') - { - $r->content_type('application/json'); - $formatted_data=&make_json($data,{pretty=>1,allow_nonref=>1,allow_blessed=>1}); - } - elsif($$requestObject{'_format'} eq 'xml') - { - $r->content_type('text/xml'); - $formatted_data=XMLout($data); - } - elsif($$requestObject{'_format'} eq 'text') - { - $logger->debug( "output data as text") if ($logger->is_debug()); - $formatted_data=$data; - } - $r->print($formatted_data); - } - } - else - { - $logger->error("error parsing api str"); - } - return Apache2::Const::OK; + if ( defined $error ) + { + return $error; + } + else + { + return $new_value; + } } -sub doFieldNormalization() +sub doEnvironmentsServicesPOST() { - my($entity,$field,$value)=@_; - my $newvalue; - $value=~s/^\ //g if defined $value; - $value=~s/\ $//g if defined $value; - my $matchers=$dbh->selectall_arrayref('select matcher,sub_value from inv_normalizer where entity_name=? and field_name=?', - {},($entity,$field)); - foreach(@$matchers) - { - - if($value=~m/$$_[0]/i) - { - $logger->debug( "matched with $$_[0] and subbing $$_[1]") if ($logger->is_debug()); - return $$_[1]; - } - } - if(ref $value eq 'ARRAY') - { - $value=join(',',@$value); - } - return $value; -} + my $requestObject = shift; + $logger->info("processing POST"); + my $dbh = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 0, RaiseError => 1 } ); + my $environment = $requestObject->{'path'}[0]; + my $service = $requestObject->{'path'}[2]; + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + my $blocked_changes = {}; + my $svc_id; + my $sth; + my $lkup_data; + my %service_updates; + my %service_attributes; + my $sql; + my $error; + + # FIXME: should this be how we handle this? No point in specifying either + # of these attributes in the request. + delete $data->{svc_id}; + if ($service) + { + $data->{name} = $service; + } + else + { + $service = $data->{name}; + } + $data->{'environment_name'} = $environment; + + if ( not defined $service ) + { + # FIXME: what's the best way to handle this? Is this the correct + # status code + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_ACCEPTABLE; + return 'Service name required'; + } - -# lifted from mod_perl2 docs, does body content read for post/put -sub read_post { - my $r = shift; - my $bb = APR::Brigade->new($r->pool,$r->connection->bucket_alloc); - my $data = ''; - my $seen_eos = 0; - do { - $r->input_filters->get_brigade($bb, Apache2::Const::MODE_READBYTES,APR::Const::BLOCK_READ, IOBUFSIZE); - for (my $b = $bb->first; $b; $b = $bb->next($b)) { - if ($b->is_eos) { - $seen_eos++; - last; - } - if ($b->read(my $buf)) { - $data .= $buf; - } - $b->remove; # optimization to reuse memory - } - } while (!$seen_eos); - $bb->destroy; - return $data; - } + eval { + # Get service_instance record for the requested service + $sql = "select svc_id from service_instance where environment_name=? and name=?"; + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, $environment, $service ); + $lkup_data = $sth->fetchall_arrayref( {}, undef ); + if ( $sth->rows ) + { + # FIXME: what's the best way to handle this? should we be able to + # turn a POST into a PUT? Is this the correct status code to use? + $dbh->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + $error = 'Service already exists'; + return; + } + &runACL( $requestObject, {}, 'services', $data, $blocked_changes ); + # if the user is not a system user, then error out now if needed + $logger->info( "blocked POST fields: " . &make_json($blocked_changes) ); + my $now = $dbh->selectcol_arrayref('select now()'); + if ( $requestObject->{'user'}->{'systemuser'} ne '1' + && scalar( keys(%$blocked_changes) ) ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + $dbh->rollback; + return 'ACL blocked change: ' . &make_json($blocked_changes); + } -#processes lexicon to get fields for an entity -sub getFieldList() -{ - my $entity=shift; - my $bare=shift || 0; - my @arr; - foreach(keys(%{$tree->{entities}->{$entity}})) - { - next if ($_ eq 'key' || $_ eq 'extends' || $_ eq 'table'); - push(@arr,$_); - } - if($valid_entities->{$entity} eq 'system' && !$bare) - { - foreach(keys(%{$tree->{entities}->{device}})) - { - next if ($_ eq 'key' || $_ eq 'extends' || $_ eq 'table'); - push(@arr,$_); - } - } - $logger->info("processed fields for $entity : " . join(',',@arr) ); - return \@arr; -} + my @columns; + my @values; -sub runACL() -{ - my($req,$r,$entity,$changes,$blocked_changes)=@_; - my($groups) = $req->{'user'}->{'groups'}; - if(ref $groups ne 'ARRAYREF') - { - $logger->debug("groups ref= ".ref $groups) if ($logger->is_debug()); - $groups = [split(',',$groups)]; - } - my $acls = $dbh->selectall_arrayref("select * from acl where entity=?", { Slice => {} },($entity)); - foreach my $field (keys(%$changes)) - { - foreach my $acl (@$acls) - { - #skip if acl group not in users grouplist - next unless(grep(/^$acl->{'acl_group'}$/,@$groups)); - # skip of the field the acl applies to isn't being changed - next unless($field eq $acl->{'field'} || $acl->{'field'} eq '*'); - $logger->info("found acl to process: " . &make_json($acl) ); - my $eval=$acl->{'logic'}; - my $out=&doEval($req,$r,$field,$changes,$acl->{'logic'}); - if($@) - { - die 'error compiling ACL'; - } - if( $out ) - { - $logger->info("acl ran and blocked"); - $blocked_changes->{ $field }=$changes->{ $field }; - delete $changes->{ $field }; - } - } - } - return ($changes,$blocked_changes); -} + for my $field (qw/name environment_name type note/) + { + if ( defined $data->{$field} ) + { + push @columns, $field; + push @values, $data->{$field}; + delete $data->{$field}; + } + } -sub doEval() -{ - my($req,$r,$f,$changes,$logic)=@_; - #$logger->debug("ACL EVAL: $logic ") - return eval($logic); -} + # Create service_instance record + $sql = sprintf( "insert into service_instance (%s) values (%s)", join( ', ', @columns ), join( ', ', map { "'$_'" } @values ) ); + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql ); + $svc_id = $sth->{mysql_insertid}; + if(scalar(keys(%$data)) > 0) + { + # Create service_instance_data records + $sql = "insert into service_instance_data " . "(svc_id, data_key, data_value) values "; -# looks for function to process the request, based on entity specification in $valid_entities and http method -sub ProcessRequest() -{ - $logger->info("detemining request process function"); - - my $requestObject=shift; - my $func='do' . $$valid_entities{$$requestObject{'entity'}} . $$requestObject{'method'}; - if($$requestObject{'method'} eq 'PUT') - { - unless ($$requestObject{'path'}[0]) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return 'no key specified'; - } - #$$requestObject{'stat'}=Apache2::Const::HTTP_ACCEPTED; - } - if($$requestObject{'method'} eq 'POST') - { - $$requestObject{'stat'}=Apache2::Const::HTTP_CREATED; - } - no strict 'refs'; - if(exists &$func) - { - $logger->info("found function $func"); - return &$func($requestObject); - } - else - { - $logger->error("no function found $func"); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return 'no entity function' - } -} + $sql .= join( ',', map { sprintf( "('%s','%s','%s')", $svc_id, $_, $data->{$_} ) } keys %$data ); -sub doColumn_lkupGET() -{ - my $requestObject=shift; - my $entity=shift || $$requestObject{'path'}[0]; - my $col=shift || $$requestObject{'path'}[1]; - - my %lkup; - if($entity eq 'system') - { - $entity = 'device'; - } - my $entity_fields=&getFieldList($entity); -$logger->info("$entity field list: " . join (',',@$entity_fields)); - foreach (@$entity_fields) { $lkup{$_}++;} -$logger->info('doing col lkup for ' . $entity . '-> ' . $col); - - my $sql; - if($lkup{$col}) - { - $sql="select distinct $col,? from $entity order by 1 limit 2000"; - } - else - { - $sql='select distinct metadata_value from device_metadata where metadata_name=? order by 1 limit 2000'; - } - my $res=$dbh->selectcol_arrayref($sql,{},($col)); - my @new; - - foreach(@$res){$_=~s/\"//g;push(@new,$_) if $_;} - - return \@new; -} + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql ); + } + insertAuditEntry( $dbh, $requestObject, 'services', "$environment/$service", 'record', '', 'CREATED', $$now[0] ); + $dbh->commit; + }; + if ($@) + { + my $errstr; + + if ( $sth->err ) + { + $errstr = $sth->err . " : " . $sth->errstr; + $logger->error($errstr); + } + else + { + $errstr = $@; + } -#audit info retreival + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; -sub doAuditGET() -{ - my $requestObject=shift; - my $entity=$$requestObject{'path'}[0]; - if($$valid_entities{$entity} eq 'System') - { - $entity='device'; - } - my $lkup=$$requestObject{'path'}[1]; - my $sql='select * from inv_audit where entity_name=? and entity_key=? order by change_time'; - return &recordFetch($requestObject,$sql,[$entity,$lkup]); -} + eval { $dbh->rollback; }; + return $errstr; + } -#special api to check current user for write access -sub doUserGET() -{ - my $requestObject=shift; - if($requestObject->{'user'}) - { - return $requestObject->{'user'}; - } - else - { - return { username=> $requestObject->{'http_auth_user'}}; - } -} + return $error if ( defined $error ); -# new traffic control api -sub doTrafficControlPOST() -{ - my $requestObject=shift; - $requestObject->{'user'}=&doGenericGET({entity=>'user',path=>['trafficcontrol']}); - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - my ($lkup_data,$lkup); - $logger->debug("TC got POST from agent") if ($logger->is_debug()); - - foreach (('fqdn','mac_address','serial_number','ip_address')) - { - # skip serial if not dell tag - next if( $_ eq 'serial_number' && length($data->{$_}) != 7 ); - $lkup= $dbh->selectall_arrayref("select * from device where $_=?", { Slice => {} },($data->{$_})); - if(scalar(@$lkup) == 1) - { - $lkup_data=$$lkup[0]; - last; - } - } - if(ref $lkup_data eq 'ARRAY' && scalar(@$lkup_data) == 0) - { - $lkup_data=''; - } - $requestObject->{'entity'}='system'; - - # if we found the entry, then setup for PUT else do POST - if($lkup_data) - { - $logger->info("TC found system " . $lkup_data->{'fqdn'} . ", doing PUT"); - $requestObject->{'path'}=[$lkup_data->{'fqdn'}]; - &doSystemPUT($requestObject); - } - else - { - $logger->info("TC doing POST for new system"); - &doSystemPOST($requestObject); - } - + $$requestObject{'headers_out'} = [ 'Location', "/cmdb_api/v1/environments/" . $environment . "/services/" . $service ]; + return; } - -sub doProvisionPcmGET() +sub doEnvironmentsServicesDELETE() { - my $requestObject=shift; - my $id = $$requestObject{'path'}[0]; + my $requestObject = shift; + $logger->info("processing DELETE"); + my $dbh = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 0, RaiseError => 1 } ); + my $environment = $requestObject->{'path'}[0]; + my $service = $requestObject->{'path'}[2]; + my $blocked_changes = {}; + my $svc_id; + my $sth; + my $lkup_data; + my %service_updates; + my %service_attributes; + my $sql; + my $error; + my $old_value; + + eval { + # Get service_instance record for the requested service + $sql = "select svc_id from service_instance where environment_name=? and name=?"; + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, $environment, $service ); + $lkup_data = $sth->fetchall_arrayref( {}, undef ); + + if ( $sth->rows == 0 ) + { + # FIXME: what's the best way to handle this? should we be able to + # turn a POST into a PUT? Is this the correct status code to use? + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + $dbh->rollback; + $error = 'There is no resource at this location'; + return; + } - if(!$id) - { - $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_ACCEPTABLE; - return {'error'=> 'missing data (id)'}; - } + $svc_id = $$lkup_data[0]->{'svc_id'}; - my ($sql,$sth,$existing_data); - $sql = 'select fqdn from device where serial_number=?'; - $sth=$dbh->prepare($sql); - $sth->execute($id); - $existing_data=$sth->fetchall_arrayref({},undef); + &runACL( $requestObject, $lkup_data, 'services', {}, $blocked_changes ); - if (scalar(@$existing_data)>1) + # if the user is not a system user, then error out now if needed + $logger->info( "blocked DELETE operation: " . &make_json($blocked_changes) ); + if ( $requestObject->{'user'}->{'systemuser'} ne '1' + && scalar( keys(%$blocked_changes) ) ) { - $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return {'error'=>'Multiple systems with the same ID'}; + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + $dbh->rollback; + $error = 'ACL blocked Delete: ' . &make_json($blocked_changes); + return; } - elsif(scalar(@$existing_data)) - { - $$requestObject{'stat'} = Apache2::Const::HTTP_OK; - return {'fqdn'=>$$existing_data[0]{'fqdn'}} - } + my $now = $dbh->selectcol_arrayref('select now()'); - my $record={}; - $$record{'inventory_component_type'} = 'system'; - $$record{'status'} = 'idle'; - $$record{'serial_number'} = $id; + $old_value = &doEnvironmentsServicesGET($requestObject); - my $name = &setNewName($record, '.' . $opt->{'prism_domain'}); + $sql = "delete from service_instance where svc_id=?"; + $sth = $dbh->prepare($sql); + executeDbStatement( $sth, $sql, $svc_id ); - $$requestObject{'stat'} = Apache2::Const::HTTP_OK; - return {'fqdn'=>$name}; -} + insertAuditEntry( $dbh, $requestObject, 'services', "$environment/$service", 'record', make_json($old_value), 'DELETED', $$now[0] ); + $dbh->commit; + }; + if ($@) + { + my $errstr; -# special api to fetch new hostname for provisioning -sub doProvisionGET() -{ - my $requestObject=shift; - my ($sql,$sth,$rv); - # this code seems pointless, why check for length 7 if we are going to remove chars? - if($$requestObject{'query'}{'serial_number'} && length($$requestObject{'query'}{'serial_number'}) == 7 ) - { - $$requestObject{'query'}{'serial_number'}=~s/^\s+//g; - $$requestObject{'query'}{'serial_number'}=~s/\s+$//g; - } - if($$requestObject{'query'}{'serial_number'} && length($$requestObject{'query'}{'serial_number'}) == 7 ) - { - $sql ='select fqdn from device where serial_number=?'; - $sth=$dbh->prepare($sql); - $rv=$sth->execute(($$requestObject{'query'}{'serial_number'})); - } - elsif(!$$requestObject{'query'}{'mac_address'}) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_FAILED_DEPENDENCY; - return 'missing data (mac_address)' - } - else - { - $sql ='select fqdn from device where mac_address=?'; - $sth=$dbh->prepare($sql); - $rv=$sth->execute(($$requestObject{'query'}{'mac_address'})); - } - # unless($$requestObject{'query'}{'serial_number'}) - # { - # $$requestObject{'stat'}=Apache2::Const::HTTP_FAILED_DEPENDENCY; - # return 'missing data (serial_number)'; - # } - # lkup system - my $data=$sth->fetchall_arrayref({},undef); - my ($hostname,$newname); - - # gather data sent in with the call - my $new={}; - foreach my $f (('rack_code','rack_position','asset_tag_number','manufacturer','product_name','serial_number','ip_address','mac_address','inventory_component_type')) - { - $$requestObject{'query'}{$f}=~s/^\s+//g; - $$requestObject{'query'}{$f}=~s/\s+$//g; - if($$requestObject{'query'}{$f}) - { - $$new{$f}=&doFieldNormalization('system',$f,$$requestObject{'query'}{$f}); - #$$new{$f}=$$requestObject{'query'}{$f}; - } - } - - # there should only be one system - if(scalar(@$data)>1) - { - $logger->warn("found too many systems in inventory: " . scalar(@$data) ); - return 'too many entries'; -#TODO do something about finding more than one system - } - elsif(scalar(@$data)) - { - $logger->info("found system in inventory: " . $$data[0]{'fqdn'}); - # call setNewName which will rename if it doesn't match - $$new{'fqdn'}=$$data[0]{'fqdn'}; - $$new{'status'}=$$data[0]{'status'}; - $newname=&setNewName($new, '.ppops.net'); - # set the IP for this system since it's coming online from provisioning vlan - # validate that the IP is on the provisioning vlan - if($$requestObject{'query'}{'ip_address'} && $$requestObject{'query'}{'ip_address'} =~ /10\.\d{1,3}\.25/) - { - $logger->info("updating IP for system entry"); - $$requestObject{'query'}{'ip_address'}=~s/^\s//g; - $$requestObject{'query'}{'ip_address'}=~s/\s$//g; - $sql='update device set ip_address=? where fqdn=?'; - my $sth=$dbh->prepare($sql); - $logger->debug("executing: $sql with $$requestObject{'query'}{'ip_address'},$newname") if ($logger->is_debug()); - my $rv=$sth->execute(($$requestObject{'query'}{'ip_address'},$newname)); - $logger->error($sth->err . " : " . $sth->errstr) if ($sth->err); - } - } - # no entry found, insert system into inventory with new name - else - { - unless($$requestObject{'query'}{'inventory_component_type'}) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_FAILED_DEPENDENCY; - return 'missing data (inventory_component_type)'; - } - $$new{'status'}='idle'; - $newname=&setNewName($new, '.ppops.net'); - if($newname=~/ERROR/) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_FAILED_DEPENDENCY; - } - } - if($$requestObject{'_format'} eq 'text') - { - $hostname=$newname; - } - else - { - $hostname={fqdn=>$newname}; - - } - $logger->info("new name assigned : $newname"); - return $hostname; - -} + if ( $sth->err ) + { + $errstr = $sth->err . " : " . $sth->errstr; + $logger->error($errstr); + } + else + { + $errstr = $@; + } -sub setNewName() -{ - my $r=shift; - my $suffix=shift || ""; - my ($sql,$parms,$where); - my $newname; - # check to see if the name is correct or if box is set to production/deployment - if ($$r{'fqdn'}!~/m\d{7}\.ppops\.net/ && $$r{'status'} ne 'production' && $$r{'status'} ne 'deployment' ) { - if (defined $$r{'mac_address'}) { - $newname = 'temp-' . $$r{'mac_address'}; - $newname =~ s/://g; - } elsif (defined $$r{'serial_number'}) { - $newname = 'temp-' . $$r{'serial_number'}; - } else { - return "ERROR: must provide serial number or MAC address"; - } - } else { - $newname=$$r{'fqdn'}; - } - - # fqdn passed in, so updating existing - if ($$r{'fqdn'}) { - $sql="update device set fqdn=?"; - push(@$parms,$newname); - } - # otherwise trying insert - else { - $sql='insert into device set fqdn=?'; - push(@$parms,$newname); - } - foreach my $f (keys(%$r)) { - if ($$r{$f} && $f ne 'fqdn') { - $sql.=", $f=? "; - push(@$parms,$$r{$f}); - } - } - # doing device update so adding where clause - if ($sql =~ /update\ device/) { - $where= " where fqdn=?"; - push(@$parms,$$r{'fqdn'}); - } - - my $dbh=DBI->connect("DBI:mysql:database=inventory;host=$DBHOST", - $DBUSER,$DBPASS,{AutoCommit=>0,RaiseError=>1}); - my $sth; - - eval { - $sth = $dbh->prepare("$sql$where"); - executeDbStatement($sth, $sql, @$parms); - - my $device_id = $sth->{mysql_insertid}; - - # We need to generate a name based on the auto-incremented - # id column if this is a new device - if ($newname =~ /^temp-/) { - $newname = sprintf "m%07d", $device_id; - $newname .= $suffix; - - $sql = "update device set fqdn=? where id=?"; - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, ($newname, $device_id)); - } - - $dbh->commit; - }; - if ($@) { - my $errstr; - - if (defined $sth && $sth->err) { - $errstr = $sth->err . " : " . $sth->errstr; - $logger->error($errstr); - } else { - $errstr = $@; - } - - $newname = "ERROR: $errstr"; - } - - return $newname; -} + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; -sub doSql(){ - my $sql=shift; - my $parms=shift; - my $dbh=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS); - - my $sth=$dbh->prepare($sql); - my $sql_out; - $logger->debug("executing: $sql with " . &make_json($parms,{allow_nonref=>1}) ) if ($logger->is_debug()); - my $rv=$sth->execute(@$parms); - $logger->error($sth->err . " : " . $sth->errstr ) if ($sth->err); - if($sth->err) - { - return {err=>$sth->err , errstr=> $sth->errstr}; - } - if($sql=~/select/) - { - $sql_out=$sth->fetchall_arrayref({},undef); - } - return { data=>$sql_out }; -} -sub recordFetch(){ - my $requestObject=shift; - my $sql=shift; - my $parms=shift; - my $return; - - my $rtn=&doSql($sql,$parms); - if($$rtn{'err'}) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return $$rtn{'err'} . " : " . $$rtn{'errstr'}; - } - my $data=$$rtn{'data'}; - - # format output inside extjs compatible object if requested - if($$requestObject{'query'}{'_extjs'}) - { - $return={ - records=>$data, - total=>scalar(@$data), - metaData=>{ - root=>'records', - totalProperty=>'total', - id=>$tree->{entities}->{$$requestObject{'entity'}}->{key}, - fields=>&getFieldList($$requestObject{'entity'}) - } - }; - if( !scalar(@{$$return{metaData}{fields}}) && scalar(@{$$return{records}}) ) - { - @{$$return{metaData}{fields}}=keys(%{$$return{records}[0]}) - } - } - elsif($$requestObject{'path'}[0]) - { - $return=$$data[0]; - } - else - { - $return=$data - } - return $return; - -} + eval { $dbh->rollback; }; + $error = $errstr; + } -# handle generic updates -sub doGenericPUT -{ - my ($sql); - my $requestObject=shift; - my $entity=$$requestObject{'entity'}; - $logger->info("processing PUT"); - my $dbs=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS,{AutoCommit=>1}); - $dbs->begin_work; - my (@sql,$parms,@errors); - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - #audit fetch record to comparison during audit - my $now=$dbh->selectcol_arrayref('select now()'); - my @entity_fields=@{&getFieldList($$requestObject{'entity'})}; - my $lkup_data=&doGenericGET($requestObject); - if(scalar(keys(%$lkup_data)) == 0) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - return 'There is no resource at this location'; - - } - if($$lkup_data{'metaData'}) - { - $lkup_data=$$lkup_data{'records'}[0] - } - elsif(ref $lkup_data eq 'ARRAYREF') - { - $lkup_data=$$lkup_data[0]; - } - - # strip out unchanged data and trigger mtime if needed - my $mtime; - foreach(@entity_fields) - { - $$data{$_}=&doFieldNormalization($entity,$_,$$data{$_}) if exists $$data{$_}; - $mtime= $$now[0] if(exists $$data{$_} && !$tree_extended->{entities}->{'system'}->{$_}->{meta} ); - delete $$data{$_} if(defined $$data{$_} && defined $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_}); - } - my $blocked_changes={}; - &runACL($requestObject,$lkup_data,$entity,$data,$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("changes: " . &make_json($data)); - $logger->info("blocked changes: " . &make_json($blocked_changes) ); - if($requestObject->{'user'}->{'systemuser'} ne '1' && scalar(keys(%$blocked_changes))) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - return 'ACL blocked change: ' . &make_json($blocked_changes); - } - if(scalar(keys(%$blocked_changes))) - { - my $change_item={ - change_ip=>$$requestObject{'ip_address'}, - change_user=>$requestObject->{'user'}->{'username'}, - change_time=>$$now[0], - entity=>$$requestObject{'entity'}, - entity_key=>$$lkup_data{$tree->{'entities'}->{$$requestObject{'entity'}}->{'key'}}, - change_content=>&make_json($blocked_changes) - }; - &doGenericPOST({ - entity=>'change_queue', - body=>&make_json($change_item), - }); - $logger->info("queued change"); - return "Change queued for approval"; - } - - foreach my $f (@entity_fields) - { - if(exists $$data{$f}) - { - if($$data{$f} eq '') - { - push(@$parms,undef); - } - else - { - push(@$parms,$$data{$f}); - } - - push(@sql,"$f=?"); - #audit check each field and record change if done - if(!defined $$lkup_data{$f} || $$data{$f} ne $$lkup_data{$f}) - { - $dbs->do('insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=?, - change_user=?, - change_ip=?', - {}, - ($entity, - $$lkup_data{$tree->{entities}->{$$requestObject{'entity'}}->{key}}, - $f, #field - $$lkup_data{$f}, #old val - $$data{$f}, # new val - $$now[0], - $requestObject->{'user'}->{'username'}, # user - $$requestObject{'ip_address'} # ip - ) - ); - } - } - } - if(scalar(@sql) == 0) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_NO_CONTENT; - $dbs->commit; - return; - } - my $sql_set=join(',',@sql); - # assemple final sql - $sql="update $entity set $sql_set where " . $tree->{entities}->{$entity}->{key} . "=?"; - push(@$parms,$$requestObject{'path'}[0]); - - ## do sql and record any errors - my $sth=$dbs->prepare($sql); - if ($sth->err) - { - push(@errors,$dbs->err . ": " . $dbs->errstr); - $logger->error($sth->err . " : " . $sth->errstr); - } - $logger->debug("executing: $sql with " . join(',',@$parms) ) if ($logger->is_debug()); - my $rv=$sth->execute(@$parms); - if ($sth->err) - { - push(@errors,$dbs->err . ": " . $dbs->errstr); - $logger->error($sth->err . " : " . $sth->errstr ); - } - - if(scalar(@errors)) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return \@errors; - } - else - { - $dbs->commit; - return &doGenericGET($requestObject); - } - + if ( defined $error ) + { + return $error; + } + else + { + return; + } } -# handle generic creations -sub doGenericPOST +sub doEnvironmentsGET() { - my ($sql); - my $requestObject=shift; - my $entity=$$requestObject{'entity'}; - $logger->info("processing POST"); - my (@sql,$parms); - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - my $blocked_changes={}; - &runACL($requestObject,{},$entity,$data,$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("blocked PUT fields: " . &make_json($blocked_changes) ); - my $now=$dbh->selectcol_arrayref('select now()'); - if($requestObject->{'user'}->{'systemuser'} ne '1' && scalar(keys(%$blocked_changes))) - { - $dbh->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - return 'ACL blocked change: ' . &make_json($blocked_changes); - } - foreach my $f (@{&getFieldList($$requestObject{'entity'})}) - { - if(exists $$data{$f}) - { - $$data{$f}=&doFieldNormalization($entity,$f,$$data{$f}); - push(@$parms,$$data{$f}); - push(@sql,"$f=?"); - } - } - my $sql_set=join(',',@sql); - $sql="insert into $entity set $sql_set"; - my $sth=$dbh->prepare($sql); - # set for error and return if db prepare had errors - $logger->error($sth->err . " : " . $sth->errstr) if ($sth->err); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR if $sth->err; - return $sth->err . " : " . $sth->errstr if ($sth->err); - #audit entry for create - $dbh->do('insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=?, - change_user=?, - change_ip=?', - {}, - ($entity, - $$data{$tree->{entities}->{$$requestObject{'entity'}}->{key}}, - 'record', #field - '', #old val - 'CREATED', # new val - $$now[0], - $requestObject->{'user'}->{'username'}, # user - $$requestObject{'ip_address'} # ip - ) - ); - - # run sql - $logger->debug("executing: $sql with " . join(',',@$parms) ) if ($logger->is_debug()); - my $rv=$sth->execute(@$parms); - #return error if db insert had errors - if($sth->err) - { - $logger->error($sth->err . " : " . $sth->errstr ); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return $sth->err . " : " . $sth->errstr; - } - $$requestObject{'headers_out'}=['Location',"/cmdb_api/v1/" . $entity . "/" . $$data{$tree->{entities}->{$$requestObject{'entity'}}->{key}}]; - return; -} - -sub doAclDELETE { - doGenericDELETE(@_); + my $requestObject = shift; + my @path = @{ $requestObject->{'path'} }; + my @parms; + my $environment; + my $service; + my $get_services = 0; + + if ( defined ($path[1]) && $path[1] eq 'services' ) + { + return &doEnvironmentsServicesGET($requestObject); + } + elsif ( $path[1] ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + } + else + { + return &doGenericGET($requestObject); + } } -sub doAclGET { - doGenericGET(@_); +sub doEnvironmentsPUT() +{ + my $requestObject = shift; + my @path = @{ $requestObject->{'path'} }; + my @parms; + my $environment; + my $service; + my $get_services = 0; + + $environment = $path[0]; + $service = $path[2]; + + if ( $path[1] eq 'services' ) + { + if ( defined $service ) + { + return &doEnvironmentsServicesPUT($requestObject); + } + else + { + $$requestObject{'stat'} = Apache2::Const::HTTP_METHOD_NOT_ALLOWED; + } + } + elsif ( $path[1] ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + } + else + { + return &doGenericPUT($requestObject); + } } -sub doAclPOST { - my ($sql); - my $requestObject=shift; - my $entity=$$requestObject{'entity'}; - $logger->info("processing POST"); - my (@sql,$parms); - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - - if($$data{'logic'}) - { - my $r = { }; - my $f = ''; - my $req = $requestObject; - my $changes = {}; - eval($$data{'logic'}); - if($@) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return "Syntax error in ACL logic: $@"; - } - } - - foreach my $f (@{&getFieldList($$requestObject{'entity'})}) - { - if(exists $$data{$f}) - { - $$data{$f}=&doFieldNormalization($entity,$f,$$data{$f}); - push(@$parms,$$data{$f}); - push(@sql,"$f=?") - } - } - my $sql_set=join(',',@sql); - $sql="insert into $entity set $sql_set"; - my $sth=$dbh->prepare($sql); - if($sth->err) - { - $logger->error($sth->err . " : " . $sth->errstr); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return $sth->err . " : " . $sth->errstr; - } - $logger->debug("executing: $sql with " . join(',',@$parms) ) if ($logger->is_debug()); - my $rv=$sth->execute(@$parms); - if($sth->err) - { - $logger->error($sth->err . " : " . $sth->errstr ); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return $sth->err . " : " . $sth->errstr; - } - $$requestObject{'headers_out'}=['Location',"/cmdb_api/v1/acl/" . $$data{'acl_id'}]; - return; - +sub doEnvironmentsPOST() +{ + my $requestObject = shift; + my @path = @{ $requestObject->{'path'} }; + my @parms; + my $get_services = 0; -} + my $environment = $path[0]; + my $service = $path[2]; -sub doAclPUT { - my ($sql); - my $requestObject=shift; - my $entity=$$requestObject{'entity'}; - $logger->info("processing PUT"); - my $dbs=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS,{AutoCommit=>1}); - $dbs->begin_work; - my (@sql,$parms,@errors); - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - #audit fetch record to comparison during audit - my $now=$dbh->selectcol_arrayref('select now()'); - my @entity_fields=@{&getFieldList($$requestObject{'entity'})}; - my $lkup_data=&doGenericGET($requestObject); - if($$lkup_data{'metaData'}) - { - $lkup_data=$$lkup_data{'records'}[0] - } - elsif(ref $lkup_data eq 'ARRAYREF') - { - $lkup_data=$$lkup_data[0]; - } - - # strip out unchanged data and trigger mtime if needed - my $mtime; - foreach(@entity_fields) - { - $$data{$_}=&doFieldNormalization($entity,$_,$$data{$_}) if exists $$data{$_}; - $mtime= $$now[0] if(exists $$data{$_} && !$tree_extended->{entities}->{'system'}->{$_}->{meta} ); - delete $$data{$_} if(exists $$data{$_} && exists $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_}); - } - my $blocked_changes={}; - &runACL($requestObject,$lkup_data,$entity,$data,$blocked_changes); - - # If the change is otherwise acceptable, and we are changing the logic, - # verify syntax - if($$data{'logic'}) - { - my $r = { }; - my $f = ''; - my $req = $requestObject; - my $changes = {}; - eval($$data{'logic'}); - if($@) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return "Syntax error in ACL logic: $@" - } - } - - # if the user is not a system user, then error out now if needed - $logger->info("changes: " . &make_json($data) ); - $logger->info("blocked changes: " . &make_json($blocked_changes) ); - if($requestObject->{'user'}->{'systemuser'} ne '1' && scalar(keys(%$blocked_changes))) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return 'ACL blocked change: ' . &make_json($blocked_changes); - } - if(scalar(keys(%$blocked_changes))) - { - my $change_item={ - change_ip=>$$requestObject{'ip_address'}, - change_user=>$requestObject->{'user'}->{'username'}, - change_time=>$$now[0], - entity=>$$requestObject{'entity'}, - entity_key=>$$lkup_data{$tree->{'entities'}->{$$requestObject{'entity'}}->{'key'}}, - change_content=>&make_json($blocked_changes) - }; - &doGenericPOST({ - entity=>'change_queue', - body=>&make_json($change_item), - }); - $logger->info("queued change"); - return "Change queued for approval"; - } - - foreach my $f (@entity_fields) - { - if(exists $$data{$f}) - { - if($$data{$f} eq '') - { - push(@$parms,undef); - } - else - { - push(@$parms,$$data{$f}); - } - - push(@sql,"$f=?"); - #audit check each field and record change if done - if($$data{$f} ne $$lkup_data{$f}) - { - $dbs->do('insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=?, - change_user=?, - change_ip=?', - {}, - ($entity, - $$lkup_data{$tree->{entities}->{$$requestObject{'entity'}}->{key}}, - $f, #field - $$lkup_data{$f}, #old val - $$data{$f}, # new val - $$now[0], - $requestObject->{'user'}->{'username'}, # user - $$requestObject{'ip_address'} # ip - ) - ); - } - } - } - my $sql_set=join(',',@sql); - # assemple final sql - $sql="update $entity set $sql_set where " . $tree->{entities}->{$entity}->{key} . "=?"; - push(@$parms,$$requestObject{'path'}[0]); - - ## do sql and record any errors - my $sth=$dbs->prepare($sql); - if ($sth->err) - { - push(@errors,$dbs->err . ": " . $dbs->errstr); - $logger->error($sth->err . " : " . $sth->errstr); - } - $logger->debug("executing: $sql with " . join(',',@$parms)) if ($logger->is_debug()); - my $rv=$sth->execute(@$parms); - if ($sth->err) - { - push(@errors,$dbs->err . ": " . $dbs->errstr); - $logger->error($sth->err . " : " . $sth->errstr); - } - - if(scalar(@errors)) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return \@errors; - } - else - { - $dbs->commit; - } - return; + if ( defined $path[1] && $path[1] eq 'services' ) + { + return &doEnvironmentsServicesPOST($requestObject); + } + elsif ( $path[1] ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + } + else + { + return &doGenericPOST($requestObject); + } } -sub doChangeQueuePOST() -{ - doGenericPOST(@_); -} -sub doChangeQueuePUT() -{ - doGenericPUT(@_); -} -sub doChangeQueueDELETE -{ - doGenericDELETE(@_); -} -#handles changequeue fetches -sub doChangeQueueGET() +sub doEnvironmentsDELETE() { - # for internal requests: - # ro = { entity, path[0] query } - - my ($keyval,$sql,$parms,$return); - $logger->info("processing GET"); - my $requestObject=shift; - # check to see if this entity requires special processing, otherwise handle with generic - # assemble sql based on input parameters - $sql="select * from change_queue ch left join device d on ch.entity_key=d.fqdn where "; - - # check for path key value and add if specified - if($$requestObject{'path'}[0]) - { - $logger->debug("found $tree->{entities}->{$$requestObject{'entity'}}->{key} : $$requestObject{'path'}[0] in url") if ($logger->is_debug()); - $sql.=" $tree->{entities}->{$$requestObject{'entity'}}->{key} like ?"; - push(@$parms,$$requestObject{'path'}[0]); - } - $logger->debug("getparms: $$requestObject{getparams}") if ($logger->is_debug()); - my $device_fields=&getFieldList('device',0); - my $change_fields=&getFieldList('change_queue',0); - if($$requestObject{getparams}) { - my @ranges=split(/[&;]/, $$requestObject{getparams}); - foreach my $range (@ranges) { - next unless $range =~ /(\w+)([!~>=<]+)(.+)/; - my $key = $1; - my $op = $2; - my $val = $3; - next if $key =~ /^_/; - next unless (grep(/^$key$/,@$device_fields) || grep(/^$key$/,@$change_fields)); - $val =~ s/'/%/g; - $op = 'LIKE' if $op eq '='; - $op = 'NOT LIKE' if $op eq '!='; - $op = 'RLIKE' if $op eq '~'; - $op = 'NOT RLIKE' if $op eq '!~'; - $logger->debug("Found param: $key $op $val") if ($logger->is_debug()); - $val =~ s/\*/%/g; - $sql.=" and " if($sql!~/where\ $/); - $sql.= grep(/^$key$/,@$device_fields) ? " d." : " ch."; - $sql.="$key $op '$val'"; - - } - } - $sql=~s/where\ // if($sql=~/where\ $/); - my $rtn= &recordFetch($requestObject,$sql,$parms); - - if(ref $rtn eq 'HASH' && defined $rtn->{metaData} && defined $rtn->{metaData}->{fields}) - { - push(@{$rtn->{metaData}->{fields}},@$device_fields); - } - return $rtn; + my $requestObject = shift; + my @path = @{ $requestObject->{'path'} }; + my @parms; + my $get_services = 0; + + my $environment = $path[0]; + my $service = $path[2]; + + if ( defined $path[1] && $path[1] eq 'services' ) + { + if ( defined $service ) + { + return &doEnvironmentsServicesDELETE($requestObject); + } + else + { + $$requestObject{'stat'} = Apache2::Const::HTTP_METHOD_NOT_ALLOWED; + } + } + elsif ( $path[1] ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + } + else + { + return &doGenericDELETE($requestObject); + } } -#handles generic fetches -sub doGenericGET() +sub isChanged() { - # for internal requests: - # ro = { entity, path[0] query } - - my ($keyval,$sql,$parms,$return); - $logger->info("processing GET"); - my $requestObject=shift; - # check to see if this entity requires special processing, otherwise handle with generic - # assemble sql based on input parameters - $sql="select * from $$requestObject{'entity'} where "; - - # check for path key value and add if specified - if($$requestObject{'path'}[0]) - { - $logger->debug("found $tree->{entities}->{$$requestObject{'entity'}}->{key} : $$requestObject{'path'}[0] in url") if ($logger->is_debug()); - $sql.=" $tree->{entities}->{$$requestObject{'entity'}}->{key} like ?"; - push(@$parms,$$requestObject{'path'}[0]); - } - elsif($$requestObject{getparams}) { - my @ranges=split(/[&;]/, $$requestObject{getparams}); - foreach my $range (@ranges) { - next unless $range =~ /(\w+)([!~>=<]+)(.+)/; - my $key = $1; - my $op = $2; - my $val = $3; - next if $key =~ /^_/; - $val =~ s/'//g; - $op = 'LIKE' if $op eq '='; - $op = 'NOT LIKE' if $op eq '!='; - $op = 'RLIKE' if $op eq '~'; - $op = 'NOT RLIKE' if $op eq '!~'; - $logger->debug("Found param: $key $op $val") if ($logger->is_debug()); - $val =~ s/\*/%/g; - $sql.=" and " if($sql!~/where\ $/); - $sql.=" $key $op '$val'"; - - } - } - $sql=~s/where\ // if($sql=~/where\ $/); - my $rec=&recordFetch($requestObject,$sql,$parms); - if(!$rec) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - return; - } - else - { - return $rec; - } + my ($old,$new,$field)=@_; + # $logger->info("TESTCHANGED--$field--$$old{$field}=$$new{$field}--"); + if (defined $$old{$field} && defined $$new{$field} && "$$old{$field}" eq "$$new{$field}") + { + return 0; + } + return 1; } -sub doGenericDELETE +sub doSystemDELETE() { - my $requestObject=shift; - my $entity=$$requestObject{'entity'}; - my ($sql,$parms); - # continue if entity key segment of the path is there - if($requestObject->{'path'}[0]) - { - my $lkup_data=&doGenericGET($requestObject); - if(!defined $lkup_data) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - return; - } - if($$lkup_data{'metaData'}) - { - $lkup_data=$$lkup_data{'records'}[0] - } - elsif(ref $lkup_data eq 'ARRAYREF') - { - $lkup_data=$$lkup_data[0]; - } - my $blocked_changes={}; - &runACL($requestObject,$lkup_data,$entity,{},$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("blocked DELETE operation: " . &make_json($blocked_changes) ); - if($requestObject->{'user'}->{'systemuser'} ne '1' && scalar(keys(%$blocked_changes))) - { - $dbh->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - return 'ACL blocked Delete: ' . &make_json($blocked_changes); - } - - $sql="delete from $requestObject->{'entity'} where $tree_extended->{entities}->{ $requestObject->{'entity'} }->{'key'} = ?"; - $parms=[ $requestObject->{'path'}[0] ]; - $dbh->do($sql,{},@$parms); - if($dbh->err) - { - $logger->error("sql: $sql with " . join(',',@$parms) ); - $logger->error($dbh->err . " : " . $dbh->errstr ) if ($dbh->err); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - } - else - { - $dbh->do('insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=now(), - change_user=?, - change_ip=?', - {}, - ($requestObject->{'entity'}, - $requestObject->{'path'}[0], - 'record', #field - substr(make_json($lkup_data),0,100). '...', #old val - 'DELETED', # new val - $requestObject->{'user'}->{'username'}, # user - $requestObject->{'ip_address'} # ip - ) - ); - } - - } - + my $requestObject = shift; + $$requestObject{'entity'} = 'device'; + return &doGenericDELETE($requestObject); } -sub parseQueryParams +# special functions to handle device and systems +sub doSystemGET() { - my ($data, $getparams, $valid_fields) = @_; + my $requestObject = shift; + my $x = 0; + my $device_fields = &getFieldList( 'device', 1 ); + my $meta_fields = &getFieldList( $$requestObject{'entity'}, 1 ); + my ( $field_sql, $join_sql, $sql, $where_sql, $parms ); + if ( $$requestObject{'path'}[0] ) + { + $where_sql = ' d.fqdn=?'; + push( @$parms, $$requestObject{'path'}[0] ); + } + my %getparams; - my @ranges=split(/[&;]/, $data); - foreach my $range (@ranges) { -# next unless $range =~ /(\w+)([!~>=<]+)(.+)/; - next unless $range =~ /(\w+)([!~>=<]+)(.*)/; + # parse custom URL query options ( for allowing ` ! etc...) + if ( $$requestObject{getparams} ) + { + my @ranges = split( /[&;]/, $$requestObject{getparams} ); + foreach my $range (@ranges) + { + # next unless $range =~ /(\w+)([!~>=<]+)(.+)/; + next unless $range =~ /(\w+)([!~>=<]+)(.*)/; my $key = $1; - my $op = $2; + my $op = $2; my $val = $3; next if $key =~ /^_/; - next unless (grep(/^$key$/,@$valid_fields)); + next unless ( grep( /^$key$/, @$device_fields ) || grep( /^$key$/, @$meta_fields ) ); $val =~ s/'//g; - $op = 'LIKE' if $op eq '='; - $op = 'NOT LIKE' if $op eq '!='; - $op = 'RLIKE' if $op eq '~'; - $op = 'NOT RLIKE' if $op eq '!~'; - $logger->debug("Found param: $key $op $val") if ($logger->is_debug()); - $$getparams{$key}{op}=$op; - $$getparams{$key}{val}=$val; - } - - return $getparams; -} - -sub doEnvironmentsServicesGET() { - my $requestObject=shift; - my $environment; - my $service; - my @parents; - my @params; - my %getparams; - my %hash; - my $environment_tag; - - $environment = $requestObject->{'path'}[0]; - $service = $requestObject->{'path'}[2]; - - my $sql = "select name, environment_name from service_instance " . - "where type='environment'"; - - my $rtn = &doSql($sql, undef); - if ($$rtn{'err'}) { - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return $$rtn{'err'} . " : " . $$rtn{'errstr'}; - } - - if ($requestObject->{'getparams'}) { - @params = split(/[&;]/, $requestObject->{'getparams'}); - parseQueryParams($requestObject->{'getparams'}, \%getparams, - [ 'type', 'name' ]); - } - - $environment_tag = 1 if defined $requestObject->{'query'}->{'_tag_environment'}; - - for my $env (@{$rtn->{'data'}}) { - my $parent = $env->{'environment_name'}; - my $name = $env->{'name'}; - - if ($parent eq $name) { - $parent = undef; - } - - $hash{$name} = $parent; - } - - $parents[0] = $environment; - while (defined $parents[-1]) { - push @parents, $hash{$parents[-1]}; - } - pop @parents; - - %hash = (); - my $list = join(', ', map { "'$_'" } @parents); - $sql = "select name, environment_name, s.svc_id, type, data_key, data_value from " . - " (select name, environment_name, svc_id, type from service_instance " . - " where type != 'environment' "; - - if (defined $service) { - $sql .= "and name like '$service' "; - } - - for my $key (keys %getparams) { - $sql .= sprintf "and %s %s '%s' ", - $key, - $getparams{$key}{op}, - $getparams{$key}{val}; - } - - $sql .= "and environment_name in ($list)) as s " . - "left join service_instance_data as d on s.svc_id = d.svc_id " . - "order by field(environment_name, $list)"; - - $rtn = &doSql($sql, undef); - if ($$rtn{'err'}) { - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return $$rtn{'err'} . " : " . $$rtn{'errstr'}; - } - - for my $data (@{$rtn->{'data'}}) { - if (not exists $hash{$data->{'name'}}) { - $hash{$data->{'name'}} = { - name => $data->{'name'}, - environment_name => $data->{'environment_name'}, - type => $data->{'type'}, - svc_id => $data->{'svc_id'}, - }; - } - - my $svc = $hash{$data->{'name'}}; - my $key = $data->{'data_key'}; - my $value = $data->{'data_value'}; - next if ((not defined $key) || exists $svc->{$key}); - - if ($environment_tag) { - $svc->{$key} = { value => $value, - environment_name => - $data->{'environment_name'} }; - } else { - $svc->{$key} = $value; - } - } - - if (scalar(keys %hash) == 1) { - return (values %hash)[0]; - } elsif (keys %hash) { - return [values %hash]; - } else { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - return; - } -} + $op = 'LIKE' if $op eq '='; + $op = 'NOT LIKE' if $op eq '!='; + $op = 'RLIKE' if $op eq '~'; + $op = 'NOT RLIKE' if $op eq '!~'; + $logger->debug("Found param: $key $op $val") if ( $logger->is_debug() ); + $getparams{$key}{op} = $op; + $getparams{$key}{val} = $val; + } + } + foreach (@$device_fields) + { + $field_sql .= "," if $field_sql; + $field_sql .= "d.$_"; -sub insertAuditEntry { - my ($dbh, $requestObject, $entity, $key, - $name, $old, $new, $time) = @_; - - my $sql = 'insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=?, - change_user=?, - change_ip=?'; - - $dbh->do($sql, {}, ($entity, $key, $name, - $old, $new, $time, - $requestObject->{user}->{username}, - $$requestObject{ip_address})); -} + #if($$requestObject{'query'}{$_}) + if ( defined $getparams{$_} ) + { + my $op = $getparams{$_}{op} ? $getparams{$_}{op} : 'LIKE'; + $where_sql .= " and " if $where_sql; + if ( $getparams{$_}{val} eq '' ) + { + $where_sql .= " (d.$_ $op ?"; + $where_sql .= " OR d.$_"; + $where_sql .= $getparams{$_}{op} eq 'LIKE' ? " is null)" : " is not null)"; + } + else + { + $where_sql .= " d.$_ $op ?"; + } + + # my $var=$$requestObject{'query'}{$_}; + # $var=~s/\*/%/g; + if ( $op !~ /RLIKE/ ) + { + $getparams{$_}{val} =~ s/\*/%/g; + } + + #push(@$parms,$var); + push( @$parms, $getparams{$_}{val} ); + } + } + foreach (@$meta_fields) + { + $field_sql .= "," if $field_sql; + if ( $_ eq 'guest_fqdns' ) + { + my $host = $getparams{fqdn} ? $getparams{fqdn}{val} : $$requestObject{'path'}[0]; + $field_sql .= " (select group_concat(fqdn) as guest_fqdns from device_metadata where metadata_value=d.fqdn and metadata_name=\"host_fqdn\" group by \"all\") as $_"; + } + else + { + $field_sql .= "m$x.metadata_value as $_"; + $join_sql .= " left join device_metadata m$x on d.fqdn=m$x.fqdn and m$x.metadata_name='$_'"; + + # if($$requestObject{'query'}{$_}) + if ( defined $getparams{$_} ) + { + my $op = $getparams{$_}{op} ? $getparams{$_}{op} : 'LIKE'; + $where_sql .= " and " if $where_sql; + if ( $getparams{$_}{val} eq '' ) + { + $where_sql .= " ( m$x.metadata_value $op ?"; + $where_sql .= " OR m$x.metadata_value"; + $where_sql .= $getparams{$_}{op} eq 'LIKE' ? " is null)" : " is not null)"; + } -sub executeDbStatement { - my ($sth, $sql, @parms) = @_; + # add a or is null as NOT matches will be true for null outer joins + elsif ( $getparams{$_}{op} =~ m/NOT/ ) + { + $where_sql .= " ( m$x.metadata_value $op ?"; + $where_sql .= " OR m$x.metadata_value is null)"; + } + else + { + $where_sql .= " m$x.metadata_value $op ?"; + } - if ($logger->is_debug()) { - my $msg; + # $where_sql.=" m$x.metadata_value like ?"; + # my $var=$$requestObject{'query'}{$_}; + # $var=~s/\*/%/g; + # push(@$parms,$var); + if ( $op !~ /RLIKE/ ) + { + $getparams{$_}{val} =~ s/\*/%/g; + } + push( @$parms, $getparams{$_}{val} ); + } + } + $x++; + } + $field_sql .= "," if $field_sql; + $field_sql .= " count(ch.id) as changes "; + $join_sql .= " left join change_queue ch on d.fqdn=ch.entity_key"; + + #$sql="select $field_sql from device d $join_sql where $where_sql group by d.fqdn"; + $sql = "select $field_sql from device d $join_sql "; + $sql .= "where $where_sql" if $where_sql; + $sql .= " group by d.fqdn"; + + my $rec = &recordFetch( $requestObject, $sql, $parms ); + if ( !$rec ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; + return; + } + else + { + return $rec; + } - if (@parms) { - $msg = "executing: $sql with " . join(',', @parms); - } else { - $msg = "executing: $sql" - } +} - $logger->debug($msg); - } +sub doSystemPUT() +{ + my $requestObject = shift; + + # this should likely be changed to AutoCommit =>0 if $dbs->begin_work doesn't prooperly enter a transaction + my $dbs = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 1 } ); + my $x = 0; + my $fqdn = $$requestObject{'path'}[0]; + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + $logger->info("processing PUT data $$requestObject{'body'}"); + my $device_fields = &getFieldList( 'device', 1 ); + my $meta_fields = &getFieldList( $$requestObject{'entity'}, 1 ); + my ( $sql, $set_sql, $parms, @errors, $rv ); + my $now = $dbh->selectcol_arrayref('select now()'); + my $lkup_data = &doSystemGET($requestObject); + + # Check to make sure the date modified/versiom of the record being submitted matches the + # the stored record. if the stored record is newer, return error + if ( defined $$data{'date_modified'} ) + { + my $date_modified_submitted = ParseDate( $$data{'date_modified'} ); + my $date_modified_stored = ParseDate( $$lkup_data{'date_modified'} ); + if ( Date_Cmp( $date_modified_submitted, $date_modified_stored ) != 0 ) + { + $$requestObject{'stat'} = Apache2::Const::HTTP_CONFLICT; + $logger->debug("Modification date of submitted record ($$data{'date_modified'}) is old: $$lkup_data{'date_modified'}") if ( $logger->is_debug() ); + return "stored record has already been modified"; + } + } + delete $$data{'date_modified'}; + delete $$data{'date_created'}; + if ( $$lkup_data{'metaData'} ) + { + $lkup_data = $$lkup_data{'records'}[0]; + } + elsif ( ref $lkup_data eq 'ARRAYREF' ) + { + $lkup_data = $$lkup_data[0]; + } + $dbs->begin_work; - return $sth->execute(@parms); -} + if ( exists $$data{'agent_type'} ) + { + $$data{'agent_reported'} = $$now[0]; + } + if ( + ( $data->{$IPADDRESSFIELD} && $data->{$IPADDRESSFIELD} ne $lkup_data->{$IPADDRESSFIELD} ) + || ( + !exists( $data->{'data_center_code'} ) + + && defined($lkup_data->{'data_center_code'}) + && length( $lkup_data->{'data_center_code'} ) == 0 + ) + ) + { + if ( $data->{$IPADDRESSFIELD} ) + { + $data->{'data_center_code'} = &lookupDC( $data->{$IPADDRESSFIELD} ); + } + else + { + $data->{'data_center_code'} = &lookupDC( $lkup_data->{$IPADDRESSFIELD} ); + } + } -sub doEnvironmentsServicesPUT(){ - my $requestObject=shift; - $logger->info("processing PUT"); - my $dbh=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST", - $DBUSER,$DBPASS,{AutoCommit=>0,RaiseError=>1}); - my $environment = $requestObject->{'path'}[0]; - my $service = $requestObject->{'path'}[2]; - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - my $blocked_changes={}; - my $svc_id; - my $sth; - my $lkup_data; - my @inserts; - my @updates; - my @deletes; - my %service_updates; - my %service_attributes; - my $sql; - my $error; - my $old_value; - my $new_value; - my $did_update; - - eval { - # Get service_instance record for the requested service - $sql = "select svc_id,type,name,environment_name,note from service_instance where environment_name=? and name=? and type!='environment'"; - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, $environment, $service); - $lkup_data = $sth->fetchall_arrayref({}, undef); - - if ($sth->rows == 0) { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - $dbh->rollback; - $error = 'There is no resource at this location'; - return; - } elsif ($sth->rows > 1) { - $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - $dbh->rollback; - $error = 'Multiple services with the same ID'; - return; - } - - $old_value = &doEnvironmentsServicesGET($requestObject); - - &runACL($requestObject,{},'services',$data,$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("blocked PUT fields: " . &make_json($blocked_changes) ); - my $now=$dbh->selectcol_arrayref('select now()'); - if ($requestObject->{'user'}->{'systemuser'} ne '1' && - scalar(keys(%$blocked_changes))) { - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - $dbh->rollback; - $error = 'ACL blocked change: ' . &make_json($blocked_changes); - return; - } - - $lkup_data = $$lkup_data[0]; - $svc_id = $lkup_data->{'svc_id'}; - - # Determine if we need to modify any fields of the service instance record - - for my $f (&getFieldList('service_instance')) { - my $value = $lkup_data->{$f}; - if (defined $value && $value ne $data->{$f}) { - $service_updates{$f} = [ $value, $data->{$f} ]; - } - - delete $data->{$f}; #ICK - } - delete $data->{'svc_id'}; #ICK - - - # Get all service_instance_data records that belong to this instance - # We don't care about inheritance for this - $sql = "select data_id,data_key,data_value " . - "from service_instance_data where svc_id=?"; - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, $svc_id); - $lkup_data = $sth->fetchall_arrayref({}, undef); - - #populate lookup data into a structure of the service_instance for reference - for my $row (@$lkup_data) { - $service_attributes{$row->{'data_key'}} = - [ $row->{'data_value'}, $row->{'data_id'} ]; - } - - my $field_list=&getFieldList('service_instance'); - for my $key (keys %$data) { - #skip if a native service_instance field - if(grep(/^$key$/,@$field_list)){ - next; - } - if (exists $service_attributes{$key}) { - if (not defined $data->{$key}) { - push @deletes, $key; - } elsif ($service_attributes{$key}[0] ne - $data->{$key}) { - push @updates, $key; - } - } else { - push @inserts, $key; - } - } - - - if (keys %service_updates) { - my $sql_set; - my @parms; - - $sql_set = join(', ', map { "$_=?" } keys %service_updates); - for my $key (keys %service_updates) { - push @parms, $service_updates{$key}[1]; - } - push @parms, $svc_id; - - $sql="update service_instance set $sql_set"; - $sql .= " where svc_id=?"; - - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, @parms); - $did_update = 1; - } - - if (@inserts) { - # Create any new service_instance_data records - $sql = "insert into service_instance_data " . - "(data_key, data_value, svc_id) values "; - $sql .= join(',', map {sprintf("('%s','%s','%s')", - $_, $data->{$_}, $svc_id)} @inserts); - - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql); - $did_update = 1; - } - - if (@updates) { - # Modify existing service_instance_data records - $sql = "update service_instance_data set " . - "data_value=? where data_key=? and svc_id=?"; - $sth = $dbh->prepare($sql); - - - for my $key (@updates) { - executeDbStatement($sth, $sql, $data->{$key}, $key, $svc_id); - } - $did_update = 1; - } - - if (@deletes) { - $sql = "delete from service_instance_data where " . - "svc_id=? and data_key in "; - $sql .= '(' . join(',', map { "'$_'" } @deletes) . ')'; - $sth = $dbh->prepare($sql); - - executeDbStatement($sth, $sql, $svc_id); - $did_update = 1; - } - - $dbh->commit; - - $new_value = &doEnvironmentsServicesGET($requestObject); - if ($did_update) { - insertAuditEntry($dbh, $requestObject, 'services', - "$environment/$service", 'record', - make_json($old_value), - make_json($new_value), - $$now[0]); - $dbh->commit; - } - }; - if ($@) { - my $errstr; - - if (defined $sth && $sth->err) { - $errstr = $sth->err . " : " . $sth->errstr; - $logger->error($errstr); - } else { - $errstr = $@; - } - - $$requestObject{'stat'} = - Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - - eval { $dbh->rollback; }; - - $error = $errstr; - } - - if (defined $error) { - return $error; - } else { - return $new_value; - } -} + # strip out unchanged data and trigger mtime if needed + my $mtime; + foreach (@$device_fields) + { + $$data{$_} = &doFieldNormalization( 'system', $_, $$data{$_} ) if exists $$data{$_}; + $mtime = $$now[0] if ( exists $$data{$_} && !$tree_extended->{'entities'}->{'system'}->{$_}->{'_meta'} ); + delete $$data{$_} if ( defined $$data{$_} && defined $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_} ); + } + foreach (@$meta_fields) + { + $$data{$_} = &doFieldNormalization( 'system', $_, $$data{$_} ) if exists $$data{$_}; + $mtime = $$now[0] if ( exists $$data{$_} && !$tree_extended->{'entities'}->{'system'}->{$_}->{'_meta'} ); + delete $$data{$_} if ( defined $$data{$_} && defined $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_} ); + } + my $blocked_changes = {}; + &runACL( $requestObject, $lkup_data, 'system', $data, $blocked_changes ); + + # if the user is not a system user, then error out now if needed + $logger->info( "changes: " . &make_json($data) ); + $logger->info( "blocked changes: " . &make_json($blocked_changes) ); + if ( defined( $requestObject->{'user'}->{'systemuser'} ) && $requestObject->{'user'}->{'systemuser'} ne '1' && scalar( keys(%$blocked_changes) ) ) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_FORBIDDEN; + return 'ACL blocked change: ' . &make_json($blocked_changes); + } + if ( scalar( keys(%$blocked_changes) ) ) + { + my $change_item = { + change_ip => $$requestObject{'ip_address'}, + change_user => $requestObject->{'user'}->{'username'}, + change_time => $$now[0], + entity => $$requestObject{'entity'}, + entity_key => $$lkup_data{ $tree->{'entities'}->{ $$requestObject{'entity'} }->{'_key'} }, + change_content => &make_json($blocked_changes) + }; + &doGenericPOST( + { + entity => 'change_queue', + body => &make_json($change_item), + } + ); + $logger->warn( "queued change for " . $$lkup_data{ $tree->{'entities'}->{ $$requestObject{'entity'} }->{'_key'} } ); + } -sub doEnvironmentsServicesPOST(){ - my $requestObject=shift; - $logger->info("processing POST"); - my $dbh=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST", - $DBUSER,$DBPASS,{AutoCommit=>0,RaiseError=>1}); - my $environment = $requestObject->{'path'}[0]; - my $service = $requestObject->{'path'}[2]; - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - my $blocked_changes={}; - my $svc_id; - my $sth; - my $lkup_data; - my %service_updates; - my %service_attributes; - my $sql; - my $error; - - # FIXME: should this be how we handle this? No point in specifying either - # of these attributes in the request. - delete $data->{svc_id}; - $data->{name} = $service; - $data->{'environment_name'} = $environment; - - if (not defined $service) { - # FIXME: what's the best way to handle this? Is this the correct - # status code - $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_ACCEPTABLE; - return 'Service name required'; - } - - eval { - # Get service_instance record for the requested service - $sql = "select svc_id from service_instance where environment_name=? and name=?"; - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, $environment, $service); - $lkup_data = $sth->fetchall_arrayref({}, undef); - - if ($sth->rows) { - # FIXME: what's the best way to handle this? should we be able to - # turn a POST into a PUT? Is this the correct status code to use? - $dbh->rollback; - $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - $error = 'Service already exists'; - return; - } - - &runACL($requestObject,{},'services',$data,$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("blocked PUT fields: " . &make_json($blocked_changes) ); - my $now=$dbh->selectcol_arrayref('select now()'); - if ($requestObject->{'user'}->{'systemuser'} ne '1' && - scalar(keys(%$blocked_changes))) { - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - $dbh->rollback; - return 'ACL blocked change: ' . &make_json($blocked_changes); - } - - my @columns; - my @values; - - for my $field (qw/name environment_name type notes/) { - if (defined $data->{$field}) { - push @columns, $field; - push @values, $data->{$field}; - delete $data->{$field}; - } - } - - # Create service_instance record - $sql = sprintf("insert into service_instance (%s) values (%s)", - join(', ', @columns), join(', ', map { "'$_'" } @values)); - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql); - $svc_id = $sth->{mysql_insertid}; - - # Create service_instance_data records - $sql = "insert into service_instance_data " . - "(svc_id, data_key, data_value) values "; - - - $sql .= join(',', map {sprintf("('%s','%s','%s')", - $svc_id, $_, $data->{$_})} keys %$data); - - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql); - - insertAuditEntry($dbh, $requestObject, 'services', "$environment/$service", - 'record', '', 'CREATED', $$now[0]); - $dbh->commit; - }; - if ($@) { - my $errstr; - - if ($sth->err) { - $errstr = $sth->err . " : " . $sth->errstr; - $logger->error($errstr); - } else { - $errstr = $@; - } - - $$requestObject{'stat'} = - Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - - eval { $dbh->rollback; }; - - return $errstr; - } - - - return $error if (defined $error); - - $$requestObject{'headers_out'}=['Location',"/cmndb_api/v1/environments/" . - $environment . "/services/" . - $service]; - return; -} + # construct update sql for device table + foreach (@$device_fields) + { + if ( exists $$data{$_} ) + { + $set_sql .= "," if $set_sql; + if ( defined( $$data{$_} ) && length( $$data{$_} ) == 0 ) + { + $set_sql .= " d.$_=NULL"; + + #push(@$parms,undef); + } + else + { + $set_sql .= " d.$_=?"; + push( @$parms, $$data{$_} ); + } + + #audit + if ( !$tree_extended->{entities}->{'system'}->{$_}->{meta} && &isChanged($lkup_data,$data,$_) ) + { + $dbs->do( + 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=?, + change_user=?, + change_ip=?', + {}, + ( + 'device', + $$lkup_data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} }, + $_, #field + $$lkup_data{$_}, #old val + $$data{$_}, # new val + $$now[0], + $requestObject->{'user'}->{'username'}, # user + $$requestObject{'ip_address'} # ip + ) + ); + } + } + } + if ($mtime) + { + $set_sql .= "," if $set_sql; + $set_sql .= " d.date_modified=?"; + push( @$parms, $mtime ); + } + $sql = "update device d set $set_sql where fqdn=?"; + push( @$parms, $fqdn ); + $logger->debug( "doing sql: $sql with " . &make_json($parms) ) if ( $logger->is_debug() ); + $dbs->do( $sql, {}, @$parms ) or push( @errors, $dbs->err . ": " . $dbs->errstr ); + if ( $dbs->err ) + { + $logger->error( $dbs->err . " : " . $dbs->errstr ) if ( $dbs->err ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + $dbs->rollback; + return \@errors; + } -sub doEnvironmentsServicesDELETE(){ - my $requestObject=shift; - $logger->info("processing DELETE"); - my $dbh=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST", - $DBUSER,$DBPASS,{AutoCommit=>0,RaiseError=>1}); - my $environment = $requestObject->{'path'}[0]; - my $service = $requestObject->{'path'}[2]; - my $blocked_changes={}; - my $svc_id; - my $sth; - my $lkup_data; - my %service_updates; - my %service_attributes; - my $sql; - my $error; - my $old_value; - - eval { - # Get service_instance record for the requested service - $sql = "select svc_id from service_instance where environment_name=? and name=?"; - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, $environment, $service); - $lkup_data = $sth->fetchall_arrayref({}, undef); - - - if ($sth->rows == 0) { - # FIXME: what's the best way to handle this? should we be able to - # turn a POST into a PUT? Is this the correct status code to use? - $$requestObject{'stat'} = Apache2::Const::HTTP_NOT_FOUND; - $dbh->rollback; - $error = 'There is no resource at this location'; - return; - } - - $svc_id = $$lkup_data[0]->{'svc_id'}; - - &runACL($requestObject,$lkup_data,'services',{},$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("blocked DELETE operation: " . &make_json($blocked_changes) ); - if ($requestObject->{'user'}->{'systemuser'} ne '1' && - scalar(keys(%$blocked_changes))) { - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - $dbh->rollback; - $error = 'ACL blocked Delete: ' . &make_json($blocked_changes); - return; - } - my $now=$dbh->selectcol_arrayref('select now()'); - - $old_value = &doEnvironmentsServicesGET($requestObject); - - $sql = "delete from service_instance where svc_id=?"; - $sth = $dbh->prepare($sql); - executeDbStatement($sth, $sql, $svc_id); - - insertAuditEntry($dbh, $requestObject, 'services', - "$environment/$service", - 'record', - make_json($old_value), - 'DELETED', $$now[0]); - $dbh->commit; - }; - if ($@) { - my $errstr; - - if ($sth->err) { - $errstr = $sth->err . " : " . $sth->errstr; - $logger->error($errstr); - } else { - $errstr = $@; - } - - $$requestObject{'stat'} = - Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - - eval { $dbh->rollback; }; - - $error = $errstr; - } - - if (defined $error) { - return $error; - } else { - return; - } -} + ## check for fqdn change and adjust internal fqdn var to reflect the new name + if ( exists $$data{'fqdn'} && $$lkup_data{'fqdn'} ne $$data{'fqdn'} ) + { + $fqdn = $$data{'fqdn'}; + $$requestObject{'path'}[0] = $fqdn; + } -sub doEnvironmentsGET(){ - my $requestObject=shift; - my @path = @{$requestObject->{'path'}}; - my @parms; - my $environment; - my $service; - my $get_services = 0; - - if ($path[1] eq 'services') { - return &doEnvironmentsServicesGET($requestObject); - } elsif ($path[1]) { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - } else { - return &doGenericGET($requestObject); - } -} + #update or insert into ip table + #doUpdateIps($fqdn,$data); -sub doEnvironmentsPUT(){ - my $requestObject=shift; - my @path = @{$requestObject->{'path'}}; - my @parms; - my $environment; - my $service; - my $get_services = 0; - - $environment = $path[0]; - $service = $path[2]; - - if ($path[1] eq 'services') { - if (defined $service) { - return &doEnvironmentsServicesPUT($requestObject); - } else { - $$requestObject{'stat'}=Apache2::Const::HTTP_METHOD_NOT_ALLOWED; - } - } elsif ($path[1]) { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - } else { - return &doGenericPUT($requestObject); - } + # do update or insert into device_metadata + foreach (@$meta_fields) + { + if ( exists $$data{$_} ) + { + $$data{$_} = &doFieldNormalization( 'system', $_, $$data{$_} ); + my $lkup = $dbs->selectrow_hashref( "select fqdn from device_metadata where metadata_name=? and fqdn=?", {}, ( $_, $fqdn ) ); + + # if exists do update + if ( $$lkup{'fqdn'} ) + { + $sql = "update device_metadata set metadata_value=? where metadata_name=? and fqdn=?"; + } + + # otherwise insert into device_metadata + else + { + $sql = "insert into device_metadata set metadata_value=?,metadata_name=?,fqdn=?,date_created=now()"; + } + @$parms = ( $$data{$_}, $_, $fqdn ); + $logger->debug( "doing sql: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + $rv = $dbs->do( $sql, {}, @$parms ) or push( @errors, $dbs->err . ": " . $dbs->errstr ); + if ( $dbs->err ) + { + $logger->error( $dbs->err . " : " . $dbs->errstr ) if ( $dbs->err ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + } + + #audit + if ( !$tree_extended->{entities}->{'system'}->{$_}->{meta} && &isChanged($lkup_data,$data,$_)) + { + $dbs->do( + 'insert into inv_audit set + entity_name=?, + entity_key=?, + field_name=?, + old_value=?, + new_value=?, + change_time=?, + change_user=?, + change_ip=?', + {}, + ( + 'device', + $$lkup_data{ $tree->{entities}->{ $$requestObject{'entity'} }->{_key} }, + $_, #field + $$lkup_data{$_}, #old val + $$data{$_}, # new val + $$now[0], + $requestObject->{'user'}->{'username'}, # user + $$requestObject{'ip_address'} # ip + ) + ); + } + } + } + if ( scalar(@errors) ) + { + $dbs->rollback; + $logger->error( "error encountered " . scalar(@errors) . ": " . &make_json( \@errors ) ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return \@errors; + } + else + { + # $$requestObject{'stat'}=Apache2::Const::HTTP_NO_CONTENT; + $dbs->commit; + return &doSystemGET($requestObject); + } } -sub doEnvironmentsPOST(){ - my $requestObject=shift; - my @path = @{$requestObject->{'path'}}; - my @parms; - my $get_services = 0; +sub applyDefaults() +{ + my $data=shift; + my $entity=shift; - my $environment = $path[0]; - my $service = $path[2]; + my $fields = &getFieldList( $entity ); + foreach (@$fields) + { + if ( ( !defined $$data{$_} || $$data{$_} eq '' ) && $tree_extended->{entities}->{ $entity }->{$_}->{_default_value} ) + { + $logger->debug("using ($entity) default value '$tree_extended->{entities}->{ $entity }->{$_}->{_default_value}' for $_"); + $$data{$_} = $tree_extended->{entities}->{ $entity }->{$_}->{_default_value}; + } + } + return $data; +} +sub doSystemPOST() +{ + my $requestObject = shift; + my $dbs = DBI->connect( "DBI:$DRIVER:database=$DATABASE;host=$DBHOST", $DBUSER, $DBPASS, { AutoCommit => 1 } ); + my $x = 0; + my $data = &eat_json( $$requestObject{'body'}, { allow_nonref => 1 } ); + my $fqdn = $$data{'fqdn'}; + $logger->info("processing POST data $$requestObject{'body'}"); + my $device_fields = &getFieldList( 'device', 1 ); + my $meta_fields = &getFieldList( $$requestObject{'entity'}, 1 ); + my ( $sql, $set_sql, $parms, @errors, $rv ); + + if ( $data->{$IPADDRESSFIELD} && !$data->{'data_center_code'} ) + { + $data->{'data_center_code'} = &lookupDC( $data->{$IPADDRESSFIELD} ); + } + $dbs->begin_work; + + #setup default values if there are any needed + $data=&applyDefaults($data,'system'); + # construct insert sql for device table + foreach (@$device_fields) + { + next if $_ eq 'created_by'; + if ( exists $$data{$_} ) + { + $$data{$_} = &doFieldNormalization( 'system', $_, $$data{$_} ); + $set_sql .= "," if $set_sql; + if ( !defined $$data{$_} || length( $$data{$_} ) == 0 ) + { + $set_sql .= " $_=NULL"; + + #push(@$parms,undef); + } + else + { + $set_sql .= " $_=?"; + push( @$parms, $$data{$_} ); + } + } + } + $sql = "insert into device set created_by='', $set_sql"; + $logger->debug( "doing sql: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + $dbs->do( $sql, {}, @$parms ) or push( @errors, $dbs->err . ": " . $dbs->errstr ); + if ( $dbs->err ) + { + $logger->error( $dbs->err . " : " . $dbs->errstr ) if ( $dbs->err ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + $dbs->rollback; + return \@errors; + } - if ($path[1] eq 'services') { - return &doEnvironmentsServicesPOST($requestObject); - } elsif ($path[1]) { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - } else { - return &doGenericPOST($requestObject); - } -} + #doUpdateIps($fqdn,$data); -sub doEnvironmentsDELETE(){ - my $requestObject=shift; - my @path = @{$requestObject->{'path'}}; - my @parms; - my $get_services = 0; - - my $environment = $path[0]; - my $service = $path[2]; - - if ($path[1] eq 'services') { - if (defined $service) { - return &doEnvironmentsServicesDELETE($requestObject); - } else { - $$requestObject{'stat'}=Apache2::Const::HTTP_METHOD_NOT_ALLOWED; - } - } elsif ($path[1]) { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - } else { - return &doGenericDELETE($requestObject); - } -} + # do update or insert into device_metadata + foreach (@$meta_fields) + { + if ( exists $$data{$_} ) + { + $$data{$_} = &doFieldNormalization( 'system', $_, $$data{$_} ) if exists $$data{$_}; + $sql = "insert into device_metadata set metadata_value=?,metadata_name=?,fqdn=?,date_created=now()"; + @$parms = ( $$data{$_}, $_, $fqdn ); + $logger->debug( "doing sql: $sql with " . join( ',', @$parms ) ) if ( $logger->is_debug() ); + $rv = $dbs->do( $sql, {}, @$parms ) or push( @errors, $dbs->err . ": " . $dbs->errstr ); + if ( $dbs->err ) + { + $logger->error( $dbs->err . " : " . $dbs->errstr ) if ( $dbs->err ); + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + } -# special functions to handle device and systems -sub doSystemGET(){ - my $requestObject=shift; - my $x=0; - my $device_fields=&getFieldList('device',1); - my $meta_fields=&getFieldList($$requestObject{'entity'},1); - my ($field_sql,$join_sql,$sql,$where_sql,$parms); - if($$requestObject{'path'}[0]) - { - $where_sql=' d.fqdn=?'; - push(@$parms,$$requestObject{'path'}[0]); - } - my %getparams; - # parse custom URL query options ( for allowing ` ! etc...) - if($$requestObject{getparams}) { - my @ranges=split(/[&;]/, $$requestObject{getparams}); - foreach my $range (@ranges) { -# next unless $range =~ /(\w+)([!~>=<]+)(.+)/; - next unless $range =~ /(\w+)([!~>=<]+)(.*)/; - my $key = $1; - my $op = $2; - my $val = $3; - next if $key =~ /^_/; - next unless (grep(/^$key$/,@$device_fields) || grep(/^$key$/,@$meta_fields)); - $val =~ s/'//g; - $op = 'LIKE' if $op eq '='; - $op = 'NOT LIKE' if $op eq '!='; - $op = 'RLIKE' if $op eq '~'; - $op = 'NOT RLIKE' if $op eq '!~'; - $logger->debug("Found param: $key $op $val") if ($logger->is_debug()); - $getparams{$key}{op}=$op; - $getparams{$key}{val}=$val; - } - } - foreach(@$device_fields) - { - $field_sql.="," if $field_sql; - $field_sql.="d.$_"; - #if($$requestObject{'query'}{$_}) - if(defined $getparams{$_}) - { - my $op = $getparams{$_}{op} ? $getparams{$_}{op} : 'LIKE'; - $where_sql.=" and " if $where_sql; - if($getparams{$_}{val} eq '') - { - $where_sql.=" (d.$_ $op ?"; - $where_sql.=" OR d.$_"; - $where_sql.=$getparams{$_}{op} eq 'LIKE' ? " is null)" : " is not null)"; - } - else - { - $where_sql.=" d.$_ $op ?"; - } - # my $var=$$requestObject{'query'}{$_}; - # $var=~s/\*/%/g; - $getparams{$_}{val} =~ s/\*/%/g; - #push(@$parms,$var); - push(@$parms,$getparams{$_}{val}); - } - } - foreach(@$meta_fields) - { - $field_sql.="," if $field_sql; - if($_ eq 'guest_fqdns') { - my $host = $getparams{fqdn} ? $getparams{fqdn}{val} : $$requestObject{'path'}[0]; - $field_sql.=" (select group_concat(fqdn) as guest_fqdns from device_metadata where metadata_value=d.fqdn and metadata_name=\"host_fqdn\" group by \"all\") as $_"; - } - else { - $field_sql.="m$x.metadata_value as $_"; - $join_sql.=" left join device_metadata m$x on d.fqdn=m$x.fqdn and m$x.metadata_name='$_'"; - # if($$requestObject{'query'}{$_}) - if(defined $getparams{$_}) - { - my $op = $getparams{$_}{op} ? $getparams{$_}{op} : 'LIKE'; - $where_sql.=" and " if $where_sql; - if($getparams{$_}{val} eq '') - { - $where_sql.=" ( m$x.metadata_value $op ?"; - $where_sql.=" OR m$x.metadata_value"; - $where_sql.=$getparams{$_}{op} eq 'LIKE' ? " is null)" : " is not null)"; - } - else - { - $where_sql.=" m$x.metadata_value $op ?"; - } - # $where_sql.=" m$x.metadata_value like ?"; - # my $var=$$requestObject{'query'}{$_}; - # $var=~s/\*/%/g; - # push(@$parms,$var); - $getparams{$_}{val} =~ s/\*/%/g; - push(@$parms,$getparams{$_}{val}); - } - } - $x++; - } - $field_sql.="," if $field_sql; - $field_sql.= " count(ch.id) as changes "; - $join_sql.=" left join change_queue ch on d.fqdn=ch.entity_key"; - - #$sql="select $field_sql from device d $join_sql where $where_sql group by d.fqdn"; - $sql="select $field_sql from device d $join_sql "; - $sql.="where $where_sql" if $where_sql; - $sql.=" group by d.fqdn"; - - - my $rec=&recordFetch($requestObject,$sql,$parms); - if(!$rec) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_NOT_FOUND; - return; - } - else - { - return $rec; - } - -} + } + } + if ( scalar(@errors) ) + { + $dbs->rollback; + $$requestObject{'stat'} = Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + return \@errors; + } + else + { + $dbs->commit; + $$requestObject{'headers_out'} = [ 'Location', "/cmdb_api/v1/system/$fqdn" ]; + } + return; -sub doSystemPUT(){ - my $requestObject=shift; - my $dbs=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS,{AutoCommit=>1}); - my $x=0; - my $fqdn=$$requestObject{'path'}[0]; - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - $logger->info("processing PUT data $$requestObject{'body'}"); - my $device_fields=&getFieldList('device',1); - my $meta_fields=&getFieldList($$requestObject{'entity'},1); - my ($sql,$set_sql,$parms,@errors,$rv); - my $now=$dbh->selectcol_arrayref('select now()'); - my $lkup_data=&doSystemGET($requestObject); - # Check to make sure the date modified/versiom of the record being submitted matches the - # the stored record. if the stored record is newer, return error - if( defined $$data{'date_modified'} ) - { - my $date_modified_submitted=ParseDate($$data{'date_modified'}); - my $date_modified_stored=ParseDate($$lkup_data{'date_modified'}); - if( Date_Cmp($date_modified_submitted,$date_modified_stored) != 0) - { - $$requestObject{'stat'}=Apache2::Const::HTTP_CONFLICT; - $logger->debug("Modification date of submitted record ($$data{'date_modified'}) is old: $$lkup_data{'date_modified'}" ) if ($logger->is_debug()); - return "stored record has already been modified"; - } - } - if($$lkup_data{'metaData'}) - { - $lkup_data=$$lkup_data{'records'}[0] - } - elsif(ref $lkup_data eq 'ARRAYREF') - { - $lkup_data=$$lkup_data[0]; - } - $dbs->begin_work; - - if(exists $$data{'agent_type'}) - { - $$data{'agent_reported'}=$$now[0]; - } - if( - ( - $data->{'ip_address'} - && $data->{'ip_address'} ne $lkup_data->{'ip_address'} - - ) - || - ( - !exists($data->{'data_center_code'}) - && length($lkup_data->{'data_center_code'})==0 - ) - ) - { - if($data->{'ip_address'}) - { - $data->{'data_center_code'}=&lookupDC($data->{'ip_address'}); - } - else - { - $data->{'data_center_code'}=&lookupDC($lkup_data->{'ip_address'}); - } - } - - # strip out unchanged data and trigger mtime if needed - my $mtime; - foreach(@$device_fields) - { - $$data{$_}=&doFieldNormalization('system',$_,$$data{$_}) if exists $$data{$_}; - $mtime= $$now[0] if(exists $$data{$_} && !$tree_extended->{'entities'}->{'system'}->{$_}->{'meta'} ); - delete $$data{$_} if(defined $$data{$_} && defined $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_}); - } - foreach(@$meta_fields) - { - $$data{$_}=&doFieldNormalization('system',$_,$$data{$_}) if exists $$data{$_}; - $mtime= $$now[0] if(exists $$data{$_} && !$tree_extended->{'entities'}->{'system'}->{$_}->{'meta'} ); - delete $$data{$_} if(defined $$data{$_} && defined $$lkup_data{$_} && $$data{$_} eq $$lkup_data{$_}); - } - my $blocked_changes={}; - &runACL($requestObject,$lkup_data,'system',$data,$blocked_changes); - # if the user is not a system user, then error out now if needed - $logger->info("changes: " . &make_json($data)); - $logger->info("blocked changes: " . &make_json($blocked_changes) ); - if($requestObject->{'user'}->{'systemuser'} ne '1' && scalar(keys(%$blocked_changes))) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_FORBIDDEN; - return 'ACL blocked change: ' . &make_json($blocked_changes); - } - if(scalar(keys(%$blocked_changes))) - { - my $change_item={ - change_ip=>$$requestObject{'ip_address'}, - change_user=>$requestObject->{'user'}->{'username'}, - change_time=>$$now[0], - entity=>$$requestObject{'entity'}, - entity_key=>$$lkup_data{$tree->{'entities'}->{$$requestObject{'entity'}}->{'key'}}, - change_content=>&make_json($blocked_changes) - }; - &doGenericPOST({ - entity=>'change_queue', - body=>&make_json($change_item), - }); - $logger->warn("queued change for " . $$lkup_data{$tree->{'entities'}->{$$requestObject{'entity'}}->{'key'}} ); - } - - # construct update sql for device table - foreach(@$device_fields) - { - if(exists $$data{$_}) - { - $set_sql.="," if $set_sql; - if( length($$data{$_})==0 ) - { - $set_sql.=" d.$_=NULL"; - #push(@$parms,undef); - } - else - { - $set_sql.=" d.$_=?"; - push(@$parms,$$data{$_}); - } - #audit - if( !$tree_extended->{entities}->{'system'}->{$_}->{meta}) - { - $dbs->do('insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=?, - change_user=?, - change_ip=?', - {}, - ('device', - $$lkup_data{$tree->{entities}->{$$requestObject{'entity'}}->{key}}, - $_, #field - $$lkup_data{$_}, #old val - $$data{$_}, # new val - $$now[0], - $requestObject->{'user'}->{'username'}, # user - $$requestObject{'ip_address'} # ip - ) - ); - } - } - } - if($mtime) - { - $set_sql.="," if $set_sql; - $set_sql.=" d.date_modified=?"; - push(@$parms,$mtime); - } - $sql="update device d set $set_sql where fqdn=?"; - push(@$parms,$fqdn); - $logger->debug("doing sql: $sql with " . &make_json($parms) ) if ($logger->is_debug()); - $dbs->do($sql,{},@$parms) or push(@errors,$dbs->err . ": " . $dbs->errstr); - if($dbs->err) - { - $logger->error($dbs->err . " : " . $dbs->errstr ) if ($dbs->err); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - $dbs->rollback; - return \@errors; - } - - ## check for fqdn change and adjust internal fqdn var to reflect the new name - if(exists $$data{'fqdn'} && $$lkup_data{'fqdn'} ne $$data{'fqdn'}) - { - $fqdn=$$data{'fqdn'}; - } - #update or insert into ip table - #doUpdateIps($fqdn,$data); - - - # do update or insert into device_metadata - foreach(@$meta_fields) - { - if(exists $$data{$_}) - { - $$data{$_}=&doFieldNormalization('system',$_,$$data{$_}); - my $lkup=$dbs->selectrow_hashref("select fqdn from device_metadata where metadata_name=? and fqdn=?",{},($_,$fqdn)); - # if exists do update - if($$lkup{'fqdn'}) - { - $sql="update device_metadata set metadata_value=? where metadata_name=? and fqdn=?"; - } - # otherwise insert into device_metadata - else - { - $sql="insert into device_metadata set metadata_value=?,metadata_name=?,fqdn=?,date_created=now()"; - } - @$parms=($$data{$_},$_,$fqdn); - $logger->debug("doing sql: $sql with " . join(',',@$parms) ) if ($logger->is_debug()); - $rv=$dbs->do($sql,{},@$parms) or push(@errors,$dbs->err . ": " . $dbs->errstr); - if($dbs->err) - { - $logger->error($dbs->err . " : " . $dbs->errstr ) if ($dbs->err); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - } - #audit - if( !$tree_extended->{entities}->{'system'}->{$_}->{meta}) - { - $dbs->do('insert into inv_audit set - entity_name=?, - entity_key=?, - field_name=?, - old_value=?, - new_value=?, - change_time=?, - change_user=?, - change_ip=?', - {}, - ('device', - $$lkup_data{$tree->{entities}->{$$requestObject{'entity'}}->{key}}, - $_, #field - $$lkup_data{$_}, #old val - $$data{$_}, # new val - $$now[0], - $requestObject->{'user'}->{'username'}, # user - $$requestObject{'ip_address'} # ip - ) - ); - } - } - } - if(scalar(@errors)) - { - $dbs->rollback; - $logger->error("error encountered " . scalar(@errors) . ": " . &make_json(\@errors) ); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return \@errors; - } - else - { - # $$requestObject{'stat'}=Apache2::Const::HTTP_NO_CONTENT; - $dbs->commit; - return &doSystemGET($requestObject); - } -} -sub doSystemPOST(){ - my $requestObject=shift; - my $dbs=DBI->connect("DBI:$DRIVER:database=$DATABASE;host=$DBHOST",$DBUSER,$DBPASS,{AutoCommit=>1}); - my $x=0; - my $data=&eat_json($$requestObject{'body'},{allow_nonref=>1}); - my $fqdn=$$data{'fqdn'}; - $logger->info("processing POST data $$requestObject{'body'}"); - my $device_fields=&getFieldList('device',1); - my $meta_fields=&getFieldList($$requestObject{'entity'},1); - my ($sql,$set_sql,$parms,@errors,$rv); - $data->{'inventory_component_type'} = 'system' unless $data->{'inventory_component_type'}; - if($data->{'ip_address'} && !$data->{'data_center_code'}) - { - - $data->{'data_center_code'}=&lookupDC($data->{'ip_address'}); - } - $dbs->begin_work; - # construct insert sql for device table - foreach(@$device_fields) - { - next if $_ eq 'created_by'; - if(exists $$data{$_}) - { - $$data{$_}=&doFieldNormalization('system',$_,$$data{$_}); - $set_sql.="," if $set_sql; - if( length($$data{$_})==0 ) - { - $set_sql.=" $_=NULL"; - #push(@$parms,undef); - } - else - { - $set_sql.=" $_=?"; - push(@$parms,$$data{$_}); - } - } - } - $sql="insert into device set created_by='', $set_sql"; - $logger->debug("doing sql: $sql with " . join(',',@$parms) ) if ($logger->is_debug()); - $dbs->do($sql,{},@$parms) or push(@errors,$dbs->err . ": " . $dbs->errstr); - if($dbs->err) - { - $logger->error($dbs->err . " : " . $dbs->errstr) if ($dbs->err); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - $dbs->rollback; - return \@errors; - } - #doUpdateIps($fqdn,$data); - - # do update or insert into device_metadata - foreach(@$meta_fields) - { - if(exists $$data{$_}) - { - $$data{$_}=&doFieldNormalization('system',$_,$$data{$_}) if exists $$data{$_}; - $sql="insert into device_metadata set metadata_value=?,metadata_name=?,fqdn=?,date_created=now()"; - @$parms=($$data{$_},$_,$fqdn); - $logger->debug("doing sql: $sql with " . join(',',@$parms) ) if ($logger->is_debug()); - $rv=$dbs->do($sql,{},@$parms) or push(@errors,$dbs->err . ": " . $dbs->errstr); - if($dbs->err) - { - $logger->error($dbs->err . " : " . $dbs->errstr ) if ($dbs->err); - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - } - - } - } - if(scalar(@errors)) - { - $dbs->rollback; - $$requestObject{'stat'}=Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; - return \@errors; - } - else - { - $dbs->commit; - $$requestObject{'headers_out'}=['Location',"/cmdb_api/v1/system/$fqdn"]; - } - return; - } sub lookupDC() { - my $ip=shift; - my $dc=$dbh->selectcol_arrayref('select data_center_code from - datacenter_subnet s - where - INET_ATON(?) BETWEEN INET_ATON( - LEFT(s.subnet, - INSTR(subnet, "/")-1)) AND - INET_ATON( - LEFT(s.subnet, - INSTR(subnet, "/")-1))+ POW(2, 32-SUBSTRING(subnet, - INSTR(subnet - , "/")+1 - ))-1',{},($ip)); - return $$dc[0]; + my $ip = shift; + my $dc = $dbh->selectcol_arrayref( + 'select data_center_code from + datacenter_subnet s + where + INET_ATON(?) BETWEEN INET_ATON( + LEFT(s.subnet, + INSTR(subnet, "/")-1)) AND + INET_ATON( + LEFT(s.subnet, + INSTR(subnet, "/")-1))+ POW(2, 32-SUBSTRING(subnet, + INSTR(subnet + , "/")+1 + ))-1', {}, ($ip) + ); + return $$dc[0]; } -sub doUpdateIps { - my $fqdn = shift; - my $data = shift; - my $ips = getExistingIps($fqdn); - my @interfaces; - foreach my $slot (grep(/mac[_]{0,1}address_.*/, keys %$data)) { - $slot =~ /mac[_]{0,1}address_(.*)/; - push(@interfaces, $1); - } - #check posted/put'd interfaces against those in DB - foreach my $interface (@interfaces) { #foreach posted interface - $logger->info("interface: $interface"); - my $mac_post= $data->{(grep(/mac[_]{0,1}address_$interface$/, keys %$data))[0]}; - my $add_post= $data->{(grep(/ip[_]{0,1}address_$interface$/, keys %$data))[0]}; - - $logger->info("\tmac_post: $mac_post"); - $logger->info("\tmac_db: $ips->{$interface}->{mac_address}"); - $logger->info("\tadd_post: $add_post"); - $logger->info("\tadd_db: $ips->{$interface}->{address}"); - doGenericPUT({entity=>'ip', body=>to_json({ fqdn=>$fqdn, address=>$add_post, interface=>$interface,mac_address=>$mac_post}),getparams=>"interface=$interface"}); - next; - unless($mac_post eq $ips->{$interface}->{mac_address}) { - #update changed mac - my $sql = "update ip set mac_address='$mac_post' where fqdn='$fqdn';"; - $logger->debug("$sql") if ($logger->is_debug()); - } - unless($add_post eq $ips->{$interface}->{address}) { - #update changed address - my $sql = "update ip set address='$add_post' where fqdn='$fqdn' and interface='$interface';"; - $logger->debug("$sql") if ($logger->is_debug()); - my $sth=$dbh->prepare($sql); - my $rv=$sth->execute(); - $logger->error($sth->err . " : " . $sth->errstr ) if ($sth->err); - } +sub doUpdateIps +{ + my $fqdn = shift; + my $data = shift; + my $ips = getExistingIps($fqdn); + my @interfaces; + foreach my $slot ( grep( /mac[_]{0,1}address_.*/, keys %$data ) ) + { + $slot =~ /mac[_]{0,1}address_(.*)/; + push( @interfaces, $1 ); + } + #check posted/put'd interfaces against those in DB + foreach my $interface (@interfaces) + { #foreach posted interface + $logger->info("interface: $interface"); + my $mac_post = $data->{ ( grep( /mac[_]{0,1}address_$interface$/, keys %$data ) )[0] }; + my $add_post = $data->{ ( grep( /ip[_]{0,1}address_$interface$/, keys %$data ) )[0] }; + + $logger->info("\tmac_post: $mac_post"); + $logger->info("\tmac_db: $ips->{$interface}->{mac_address}"); + $logger->info("\tadd_post: $add_post"); + $logger->info("\tadd_db: $ips->{$interface}->{address}"); + doGenericPUT( { entity => 'ip', body => to_json( { fqdn => $fqdn, address => $add_post, interface => $interface, mac_address => $mac_post } ), getparams => "interface=$interface" } ); + next; + unless ( $mac_post eq $ips->{$interface}->{mac_address} ) + { + #update changed mac + my $sql = "update ip set mac_address='$mac_post' where fqdn='$fqdn';"; + $logger->debug("$sql") if ( $logger->is_debug() ); } -} - + unless ( $add_post eq $ips->{$interface}->{address} ) + { + #update changed address + my $sql = "update ip set address='$add_post' where fqdn='$fqdn' and interface='$interface';"; + $logger->debug("$sql") if ( $logger->is_debug() ); + my $sth = $dbh->prepare($sql); + my $rv = $sth->execute(); + $logger->error( $sth->err . " : " . $sth->errstr ) if ( $sth->err ); -1; + } + } +} +1; ## HTTP codes for reference #define RESPONSE_CODES 57 @@ -2749,4 +3147,3 @@ sub doUpdateIps { #define HTTP_VARIANT_ALSO_VARIES 506 #define HTTP_INSUFFICIENT_STORAGE 507 #define HTTP_NOT_EXTENDED 510 - diff --git a/cmdb_test.csv b/cmdb_test.csv deleted file mode 100644 index 5957117..0000000 --- a/cmdb_test.csv +++ /dev/null @@ -1 +0,0 @@ -testname,notes,entity,entity_key,data,method,host:port,baseurl,user/pass,returncode,result type,result check create system,,system,test.test.com,"{""fqdn"":""test.test.com"",""ip_address"":""10.10.0.1"",""drac"":""10.10.10.1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/system/test.test.com get system,,system,test.test.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,ip_address=10.10.0.1 modify system,,system,test.test.com,"{""notes"":""testnote""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,notes=testnote lookup nonexistent system,,system,no.system.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, ,"there is no delete function, this will always fail",system,test.test.com,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, verify via audit ,,inv_audit,,"{""entity_key"":""test.test.com"",""field_name"":""notes"",""new_value"":""testnote""}",GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,new_value=testnote create datacenter_subnet,,datacenter_subnet,,"{""data_center_code"":""SC4"",""subnet"":""10.10.0.0/24"",""notes"":""testsubnet"",""id"":1000}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location~/cmdb_api/v1/datacenter_subnet/1000 edit datacenter_subnet,,datacenter_subnet,1000,"{""notes"":""testnote""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,notes=testnote test dc autoassign precheck,verify there is no dc for system,system,test.test.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,data_center_code= dc autoassign,"change system ip, and dc should get edited",system,test.test.com,"{""ip_address"":""10.10.0.12""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,data_center_code=SC4 delete datacenter_subnet,,datacenter_subnet,1000,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, test delete of not found,,datacenter_subnet,1000,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, ,,,,,,,,,,, create service_instance,,service_instance,999,"{""svc_id"":999,""environment_name"":""production"",""name"":""testservice_record"",""type"":""test""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/service_instance/999 edit service_instance,,service_instance,999,"{""note"":""testnote""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,note=testnote ,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, create service_instance_data,,service_instance_data,9999,"{""svc_id"":999,""data_id"":9999,""data_key"":""test_attribute"",""data_value"":""testvalue""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/service_instance_data/9999 edit service_instance_data,,service_instance_data,9999,"{""data_value"":""newdatavalue""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,data_value=newdatavalue ,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, delete service_instance_data,,service_instance_data,9999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, delete service_instance,,service_instance,999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, ,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, inv_normalizer,,inv_normalizer,999,"{""id"":999,""field_name"":""product_name"",""matcher"":""testname"",""sub_value"":""SpecialTestProduct"",""entity_name"":""system""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/inv_normalizer/999 do edit against normalizer,,system,test.test.com,"{""product_name"":""testname2""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,product_name=SpecialTestProduct delete normalizer,,inv_normalizer,999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, ,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, lookup nonexistent role,,role,norole,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, add role,,role,test::role,"{""role_id"":""test::role"",""role_name"":""Unit Test role"",""blessed"":1}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/role/test::role test role,,role,test::role,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,role_name=Unit Test role modify role,,role,test::role,"{""role_name"":""test role for unit testing""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,role_name=test role for unit testing delete role,,role,test::role,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, delete nonexistenet role,,role,norole,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, ,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, add acl,,acl,999,"{""acl_group"":""readonly"",""acl_id"":999,""entity"":""system"",""field"":""drac"",""logic"":""1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/acl/999 verify acl,,acl,999,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,entity=system make change against acl,,system,test.test.com,"{""drac"":""20.20.20.20""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,403,, verify acl blocked change,,system,test.test.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,,drac=10.10.10.1 delete acl,,acl,999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, ,,,,,,,,,,, "FINISHING: delete system NOTE: this will always fail, manually delete test.test.com entry",,system,test.test.com,,DELETE,,,,200,, \ No newline at end of file diff --git a/cmdb_test.pl b/cmdb_test.pl index 8fc483d..b773fa3 100644 --- a/cmdb_test.pl +++ b/cmdb_test.pl @@ -22,20 +22,23 @@ use LWP::UserAgent; use Getopt::Std; my %opt; -getopts('f:dprH',\%opt); +getopts('f:dprHh:R:',\%opt); my $DEBUG = $opt{'d'} ? 1 : 0; my $FILE = $opt{'f'} ? $opt{'f'} : ""; my $PARSEONLY = $opt{'p'} ? 1 : 0; my $RUNTESTS = $opt{'r'} ? 1 : 0; my $HALT = $opt{'H'} ? 1 : 0; +my $HOSTPORT= $opt{'h'} ? $opt{'h'} : undef; +my $REALM = $opt{'R'} ? $opt{'R'} : 'Operations Only'; if(length($FILE) == 0) { print "usage: inv_test.pl -f - -f: csv file with testcases (see inv_tests.csv) + -f: csv file with testcases (see cmdb_tests.csv) -p: parse testcase file only -r: run tests -d: debug + -h: alternate host/port (ex: https://localhost) "; exit; } @@ -101,6 +104,10 @@ print "\n"; } print "Complete: Total: " . ($$testResults{'pass'} + $$testResults{'fail'}) . " Pass: $$testResults{'pass'} Fail: $$testResults{'fail'}\n"; + if($$testResults{'fail'} > 0) + { + exit 1; + } } sub makeUrlQueryStr() @@ -123,7 +130,8 @@ () my @expected_result; ### assemble request from test data - my $testurl=$test->{'host:port'} . $test->{'baseurl'} . $test->{'entity'} . '/'; + my $hostport_config= $HOSTPORT || $test->{'host:port'}; + my $testurl=$hostport_config . $test->{'baseurl'} . $test->{'entity'} . '/'; if ($test->{'method'} ne 'POST') { $testurl.=$test->{'entity_key'}; @@ -132,9 +140,9 @@ () my $user=$1; my $pass=$2; my $result=''; - $test->{'host:port'}=~m|//(.*)|; + $hostport_config=~m|//(.*)|; my $hostport=$1; - $ua->credentials($hostport,'Authorized Personnel Only',$user,$pass); + $ua->credentials($hostport,$REALM,$user,$pass); if($test->{'method'} eq 'GET') { if($test->{'data'}) @@ -151,7 +159,7 @@ () $response = $ua->request($request); } print "### executing test: " . $test->{'testname'} . " with url (" . $test->{'method'} . "): $testurl" ; - + print "DEBUG: got status: $response->code content: \n" . $response->content if $DEBUG; if($response->code == 501) { $result.="\nHTTP ERROR: " . $response->status_line; diff --git a/cmdb_tests.csv b/cmdb_tests.csv new file mode 100644 index 0000000..e15fd37 --- /dev/null +++ b/cmdb_tests.csv @@ -0,0 +1,58 @@ +testname,notes,entity,entity_key,data,method,host:port,baseurl,user/pass,returncode,result type,result check +create system,,system,test.test.com,"{""fqdn"":""test.test.com"",""ipaddress"":""10.10.0.1"",""ipmi_ip"":""10.10.10.1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/system/test.test.com +get system,,system,test.test.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,ipaddress=10.10.0.1 +modify system,,system,test.test.com,"{""notes"":""testnote""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,notes=testnote +lookup nonexistent system,,system,no.system.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, +,"there is no delete function, this will always fail",system,test.test.com,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +verify via audit ,,inv_audit,,"{""entity_key"":""test.test.com"",""field_name"":""notes"",""new_value"":""testnote""}",GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,new_value=testnote +rename system,,system,test.test.com,"{""fqdn"":""test2.test.com"",""note"":""rename""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,fqdn=test2.test.com +change system name via trafficcontrol,,fact,,"{""fqdn"":""test.test.com"",""ipaddress"":""10.10.0.1"",""note"":""rename""}",POST,http://localhost:80,/cmdb_api/v1/,,201,data,fqdn=test.test.com +create datacenter,,data_center,DC1,"{""data_center_code"":""DC1"",""data_center_vendor"":""sample vendor""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/data_center/DC1 +,,,,,,,,,,, +create datacenter_subnet,,datacenter_subnet,,"{""data_center_code"":""DC1"",""subnet"":""10.10.0.0/24"",""notes"":""testsubnet"",""id"":1000}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location~/cmdb_api/v1/datacenter_subnet/1000 +edit datacenter_subnet,,datacenter_subnet,1000,"{""notes"":""testnote""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,notes=testnote +test dc autoassign precheck,verify there is no dc for system,system,test.test.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,data_center_code= +dc autoassign,"change system ip, and dc should get edited",system,test.test.com,"{""ipaddress"":""10.10.0.12""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,data_center_code=DC1 +delete datacenter_subnet,,datacenter_subnet,1000,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +test delete of not found,,datacenter_subnet,1000,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, +,,,,,,,,,,, +create service_instance,,service_instance,999,"{""svc_id"":999,""environment_name"":""production"",""name"":""testservice_record"",""type"":""test""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/service_instance/999 +edit service_instance,,service_instance,999,"{""note"":""testnote""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,note=testnote +,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, +create service_instance_data,,service_instance_data,9999,"{""svc_id"":999,""data_id"":9999,""data_key"":""test_attribute"",""data_value"":""testvalue""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/service_instance_data/9999 +edit service_instance_data,,service_instance_data,9999,"{""data_value"":""newdatavalue""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,data_value=newdatavalue +,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, +delete service_instance_data,,service_instance_data,9999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete service_instance,,service_instance,999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, +inv_normalizer,,inv_normalizer,999,"{""id"":999,""field_name"":""productname"",""matcher"":""testname"",""sub_value"":""SpecialTestProduct"",""entity_name"":""system""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/inv_normalizer/999 +do edit against normalizer,,system,test.test.com,"{""productname"":""testname2""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,productname=SpecialTestProduct +delete normalizer,,inv_normalizer,999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, +lookup nonexistent role,,role,norole,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, +add role,,role,test::role,"{""role_id"":""test::role"",""role_name"":""Unit Test role"",""blessed"":1}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/role/test::role +test role,,role,test::role,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,role_name=Unit Test role +modify role,,role,test::role,"{""role_name"":""test role for unit testing""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,role_name=test role for unit testing +delete role,,role,test::role,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete nonexistenet role,,role,norole,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, +,,,,,,http://localhost:80,/cmdb_api/v1/,readonly/readonly,,, +add acl,,acl,999,"{""acl_group"":""readonly"",""acl_id"":999,""entity"":""system"",""field"":""ipmi_ip"",""logic"":""1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/acl/999 +verify acl,,acl,999,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,entity=system +make change against acl,,system,test.test.com,"{""ipmi_ip"":""20.20.20.20"",""note"":""testnoate""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,403,, +create test env,,environments,,"{""name"":""testenv1"",""environment_name"":""testenv1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/environments/testenv1 +create environment/service,,environments/testenv1/services,,"{""name"":""servicetest"",""type"":""service"",""testattribute"":""12345"",""environment_name"":""testenv1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/environments/testenv1/services/servicetest +create child env,,environments,,"{""name"":""testenv2"",""environment_name"":""testenv1""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/environments/testenv2 +validate inherited service,,environments/testenv2/services,servicetest,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,environment_name=testenv1 +create service override,,environments/testenv2/services,,"{""name"":""servicetest"",""type"":""service"",""environment_name"":""testenv2""}",POST,http://localhost:80,/cmdb_api/v1/,readonly/readonly,201,header,Location=/cmdb_api/v1/environments/testenv2/services/servicetest +create service attribute override,,environments/testenv2/services,servicetest,"{""testattribute"":""testvalue""}",PUT,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,testattribute=testvalue +verify acl blocked change,,system,test.test.com,,GET,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,data,ipmi_ip=10.10.10.1 +delete acl,,acl,999,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +,,,,,,,,,,, +,,,,,,,,,,, +delete child env,,environments,testenv2,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete test service,,environments/testenv1/services,servicetest,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete test service override,,environments/testenv2/services,servicetest,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete test env,,environments,testenv1,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete system,,system,test.test.com,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, +delete renamed non existant test system,,system,test2.test.com,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,404,, +delete datacenter,,data_center,DC1,,DELETE,http://localhost:80,/cmdb_api/v1/,readonly/readonly,200,, diff --git a/dist b/dist new file mode 100755 index 0000000..2c36adc --- /dev/null +++ b/dist @@ -0,0 +1,17 @@ +#!/bin/bash + +# Prepare 'distribution' (source tarball) +# This is a placeholder for what should really be a Perl module +# prepared with Module::Build + +set -e + +NAME=cmdb-api +VERSION=0.25 + +rm -rf $NAME-$VERSION $NAME-*.tar.gz +files=$(ls -1 | grep -v ^dist\$ | grep -v ^clean\$) +mkdir $NAME-$VERSION +echo "+++ files:" $files +cp -R $files $NAME-$VERSION +tar cvzf $NAME-$VERSION.tar.gz $NAME-$VERSION && rm -rf $NAME-$VERSION diff --git a/init.pl b/init.pl index bd602b8..59b1d76 100644 --- a/init.pl +++ b/init.pl @@ -16,7 +16,6 @@ # limitations under the License. ############ -use lib qw(/var/www/cmdb_api /opt/pptools); -use ppenv; +use lib qw(/var/www/cmdb_api /opt/cmdb /opt/cmdb-api /opt/cmdb_api); use cmdb_api; 1; diff --git a/initial_schema.sql b/initial_schema.sql index eca6a71..6472598 100644 --- a/initial_schema.sql +++ b/initial_schema.sql @@ -205,6 +205,18 @@ CREATE TABLE `role` ( PRIMARY KEY (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- +-- Table structure for table `environments` +-- + +DROP TABLE IF EXISTS `environments`; +CREATE TABLE `environments` ( + `name` varchar(75) NOT NULL, + `note` varchar(255) DEFAULT NULL, + `environment_name` varchar(100) DEFAULT NULL, + PRIMARY KEY (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- -- Table structure for table `service_instance` -- @@ -229,8 +241,10 @@ CREATE TABLE `service_instance_data` ( `svc_id` mediumint(9) NOT NULL, `data_key` varchar(45) NOT NULL, `data_value` varchar(255) DEFAULT NULL, - PRIMARY KEY (`data_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; + PRIMARY KEY (`data_id`), + KEY `svc_id` (`svc_id`), + CONSTRAINT `service_instance_data_ibfk_1` FOREIGN KEY (`svc_id`) REFERENCES `service_instance` (`svc_id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=10079 DEFAULT CHARSET=utf8 -- -- Table structure for table `snat` diff --git a/lexicon.json.sample b/lexicon.json.sample new file mode 100644 index 0000000..d95339a --- /dev/null +++ b/lexicon.json.sample @@ -0,0 +1,902 @@ +{ + "entities": { + "acl": { + "acl_group": { + "_format": "string", + "_label": "Group" + }, + "acl_id": { + "_format": "int", + "_label": "ID" + }, + "entity": { + "_format": "string", + "_label": "Entity" + }, + "field": { + "_format": "string", + "_label": "Field" + }, + "_field_order": "acl_id,acl_group,entity,field,logic", + "_key": "acl_id", + "logic": { + "_format": "string", + "_label": "Block Logic" + } + }, + "change_queue": { + "change_content": { + "_format": "string", + "_label": "Change" + }, + "change_ip": { + "_format": "string", + "_label": "IP Address" + }, + "change_time": { + "_format": "string", + "_label": "Time" + }, + "change_user": { + "_format": "string", + "_label": "User" + }, + "entity": { + "_format": "string", + "_label": "Entity" + }, + "entity_key": { + "_format": "string", + "_label": "Key" + }, + "id": { + "_format": "string", + "_label": "ID" + }, + "_key": "id" + }, + "data_center": { + "data_center_city": { + "_label": "City" + }, + "data_center_code": { + "_format": "string", + "_label": "Datacenter", + "_required": "true" + }, + "data_center_country": { + "_label": "Country" + }, + "data_center_phone": { + "_label": "Phone" + }, + "data_center_vendor": { + "_label": "Vendor" + }, + "_field_order": "data_center_code,data_center_vendor,data_center_city,data_center_country,data_center_telephone", + "_key": "data_center_code" + }, + "datacenter_subnet": { + "data_center_code": { + "_flavor": "config", + "_required": "false" + }, + "_field_order": "id,subnet,ipv6_subnet,data_center_code,nodes,az_id", + "gateway": { + "_flavor": "config", + "_format": "string", + "_label": "Gateway", + "_required": "false" + }, + "id": { + "_flavor": "config", + "_format": "string", + "_label": "ID", + "_required": "true" + }, + "_key": "id", + "notes": { + "_flavor": "config", + "_format": "text", + "_label": "Notes", + "_required": "false" + }, + "subnet": { + "_flavor": "config", + "_format": "string", + "_label": "Subnet", + "_required": "true" + }, + "vlan": { + "_flavor": "config", + "_format": "string", + "_label": "VLAN", + "_required": "false" + } + }, + "device": { + "agent_reported": { + "_flavor": "_fact", + "_label": "Agent Reported", + "_meta": "true", + "_required": "false", + "_type": "date" + }, + "cage_code": { + "_field_category": "HW Info", + "_label": "Cage", + "_required": "false" + }, + "created_by": { + "_label": "Created by", + "_required": "false" + }, + "data_center_code": { + "_enumeration": { + "attribute": "data_center_code", + "entity": "data_center", + "enumerator": [ + "BA1", + "DC1", + "ETN", + "TEST01" + ] + }, + "_flavor": "_fact", + "_format": "string", + "_label": "Datacenter", + "_required": "true" + }, + "date_created": { + "_label": "Created at", + "_required": "false", + "_type": "date" + }, + "date_modified": { + "_label": "Modified at", + "_required": "false", + "_type": "date" + }, + "fqdn": { + "_fact": "fqdn", + "_field_category": "main", + "_flavor": "config", + "_format": "string", + "_label": "Name", + "_required": "true" + }, + "inventory_component_type": { + "_default_value": "system", + "_enumeration": { + "enumerator": [ + "blade_chassis", + "console_server", + "firewall", + "load_balancer", + "network_switch", + "other", + "power_strip", + "router", + "rsa_appliance", + "storage_head", + "storage_shelf", + "system", + "vpn" + ] + }, + "_field_category": "main", + "_flavor": "config", + "_format": "string", + "_label": "Type", + "_required": "true", + "_validation": { + "is_string": {} + } + }, + "ipaddress": { + "_fact": "betterip,ipaddress,ip_address", + "_field_category": "main", + "_format": "string", + "_label": "IP Address", + "_required": "true", + "_validation": { + "regex": "/.*/" + } + }, + "_key": "fqdn", + "macaddress": { + "_flavor": "_fact", + "_format": "string", + "_label": "MAC Address", + "_required": "true", + "_validation": { + "regex": "/.*/" + } + }, + "manufacturer": { + "_enumeration": { + "attribute": "manufacturer", + "entity": "system", + "enumerator": [ + "Dell", + "HP", + "iXsystems", + "Supermicro" + ], + "forceselect": "false" + }, + "_fact": "manufacturer", + "_field_category": "HW Info", + "_format": "string", + "_label": "Manufacturer" + }, + "productname": { + "_enumeration": { + "attribute": "productname", + "entity": "system", + "enumerator": [ + "SpecialTestProduct" + ], + "forceselect": "false" + }, + "_fact": "productname", + "_field_category": "HW Info", + "_format": "string", + "_label": "Product" + }, + "rack_code": { + "_field_category": "HW Info", + "_label": "Rack", + "_required": "false" + }, + "rack_position": { + "_field_category": "HW Info", + "_flavor": "config", + "_format": "string", + "_label": "Rack Position", + "_validation": { + "is_int": {}, + "less_than_or_equal_to": "42" + } + }, + "serial_number": { + "_fact": "serial_number", + "_field_category": "HW Info", + "_format": "string", + "_label": "Serial" + }, + "status": { + "_enumeration": { + "enumerator": [ + "production", + "deployment", + "allocated", + "idle", + "in transit", + "degraded", + "decommissioned", + "disposed" + ] + }, + "_field_category": "main", + "_flavor": "config", + "_format": "string", + "_label": "Status", + "_required": "false", + "_default_value": "idle", + "_validation": { + "is_string": {} + } + } + }, + "environments": { + "environment_name": { + "_enumeration": { + "attribute": "name", + "entity": "environments", + "enumerator": [ + "lab", + "production", + "secondlevel", + "testing" + ] + }, + "_field_category": "main", + "_label": "Parent Environment", + "_required": "true" + }, + "_key": "name", + "name": { + "_label": "Name", + "_required": "true" + }, + "note": { + "_label": "Note", + "_required": "false" + } + }, + "environmentservice": { + "environment_name": { + "_enumeration": { + "attribute": "name", + "entity": "environments", + "enumerator": [ + "lab", + "production", + "secondlevel", + "testing" + ] + }, + "_field_category": "main", + "_label": "Environment", + "_required": "false" + }, + "_field_order": "name,type,note,environment_name", + "_key": "name", + "name": { + "_label": "SVC Name", + "_required": "true" + }, + "note": { + "_label": "Note", + "_required": "false" + }, + "type": { + "_label": "Type", + "_required": "false" + } + }, + "hardware_model": { + "hardware_class": { + "_label": "HW Class", + "_required": "false" + }, + "_key": "hardware_class", + "manufacturer": { + "_format": "string", + "_label": "Manufacturer" + }, + "power_supply_count": { + "_label": "Pwr Supply Count", + "_required": "false" + }, + "power_supply_watts": { + "_format": "float", + "_label": "Power Supply(Watts)", + "_validation": { + "is_float": {} + } + }, + "product_name": { + "_format": "string", + "_label": "Product" + }, + "size": { + "_format": "int", + "_label": "Size (RU)", + "_required": "false" + } + }, + "inv_audit": { + "change_ip": { + "_format": "string", + "_label": "IP Address" + }, + "change_time": { + "_format": "string", + "_label": "Time" + }, + "change_user": { + "_format": "string", + "_label": "User" + }, + "entity_key": { + "_format": "string", + "_label": "Key" + }, + "entity_name": { + "_format": "string", + "_label": "Entity" + }, + "field_name": { + "_format": "string", + "_label": "Field" + }, + "_key": "entity_key", + "new_value": { + "_format": "string", + "_label": "New Value" + }, + "old_value": { + "_format": "string", + "_label": "Old Value" + } + }, + "inv_normalizer": { + "entity_name": { + "_format": "string", + "_label": "Entity", + "_required": "true" + }, + "field_name": { + "_format": "string", + "_label": "Field", + "_required": "true" + }, + "_field_order": "id,entity_name,field_name,matcher,sub_value", + "id": { + "_format": "int", + "_label": "ID", + "_required": "true" + }, + "_key": "id", + "matcher": { + "_format": "string", + "_label": "Regex Matcher", + "_required": "true" + }, + "sub_value": { + "_format": "string", + "_label": "Substitute Value", + "_required": "true" + } + }, + "rack": { + "cage_code": { + "_required": "true" + }, + "data_center_code": { + "_required": "true" + }, + "_key": "rack_code", + "rack_code": { + "_required": "false" + } + }, + "role": { + "_field_order": "role_id,role_name", + "_key": "role_id", + "role_id": { + "_format": "string", + "_label": "Role", + "_required": "true" + }, + "role_name": { + "_label": "Description", + "_required": "true" + } + }, + "service_instance": { + "environment_name": { + "_enumeration": { + "attribute": "name", + "entity": "environments", + "enumerator": [ + "lab", + "production", + "secondlevel", + "testing" + ] + }, + "_field_category": "main", + "_label": "Environment", + "_required": "false" + }, + "_field_order": "svc_id,name,type,note,environment_name", + "_key": "svc_id", + "name": { + "_label": "SVC Name", + "_required": "true" + }, + "note": { + "_label": "Note", + "_required": "false" + }, + "svc_id": { + "_label": "SVC ID", + "_required": "true" + }, + "type": { + "_label": "Type", + "_required": "false" + } + }, + "service_instance_data": { + "data_id": { + "_label": "Data ID", + "_required": "true" + }, + "data_key": { + "_label": "Key", + "_required": "true" + }, + "data_value": { + "_label": "Value", + "_required": "true" + }, + "_key": "data_id", + "svc_id": { + "_label": "SVC ID", + "_required": "true" + } + }, + "system": { + "agent_type": { + "_flavor": "_fact", + "_label": "Agent Type", + "_required": "false" + }, + "asset_tag_number": { + "_field_category": "HW Info", + "_flavor": "config", + "_format": "string", + "_label": "Asset Tag" + }, + "audit_info": { + "_flavor": "config", + "_format": "string", + "_label": "Phys Audit", + "_required": "false" + }, + "bios_vendor": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Bios Vendor", + "_required": "false" + }, + "bios_version": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Bios Version", + "_required": "false" + }, + "cloud": { + "_label": "Cloud", + "_meta": "true", + "_required": "false" + }, + "config_agent_output": { + "_field_category": "Puppet", + "_format": "text", + "_label": "Puppet Output", + "_meta": "true", + "_required": "false" + }, + "config_agent_status": { + "_field_category": "Puppet", + "_label": "Puppet Status", + "_meta": "true", + "_required": "false" + }, + "config_agent_summary": { + "_field_category": "Puppet", + "_label": "Puppet Summary", + "_meta": "true", + "_required": "false" + }, + "config_agent_timestamp": { + "_field_category": "Puppet", + "_label": "Puppet Timestamp", + "_meta": "true", + "_required": "false", + "_type": "date" + }, + "disk_drive_count": { + "_field_category": "HW Info", + "_label": "Drive Count", + "_required": "false" + }, + "drac": { + "_field_category": "HW Info", + "_label": "DRAC", + "_required": "false" + }, + "drac_macaddress": { + "_field_category": "HW Info", + "_label": "Drac MacAddress", + "_required": "false" + }, + "drac_version": { + "_field_category": "HW Info", + "_label": "Drac Version", + "_required": "false" + }, + "environment_name": { + "_default_value": "production", + "_enumeration": { + "attribute": "name", + "entity": "environments", + "enumerator": [ + "lab", + "production", + "secondlevel", + "testing" + ] + }, + "_field_category": "main", + "_label": "Environment", + "_required": "false" + }, + "_extends": "/lexicon/entities/device", + "_field_order": "fqdn,status,inventory_component_type,roles,ipaddress,environment_name,svc_id,tags,notes", + "guest_fqdns": { + "fqdn": { + "_required": "false" + }, + "_label": "Guest Hosts", + "_type": "array" + }, + "hardware_class": { + "_enumeration": { + "attribute": "hardware_class", + "entity": "hardware_model", + "enumerator": [ + "1USystem", + "2USystem", + "3USystem", + "4USystem" + ] + }, + "_field_category": "HW Info", + "_label": "Class", + "_required": "false" + }, + "host_fqdn": { + "_label": "Parent Host", + "_required": "false" + }, + "image": { + "_label": "System Image", + "_meta": "true", + "_required": "false" + }, + "interfaces": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "string", + "_label": "NICs", + "_required": "false" + }, + "ipmi_ip": { + "_field_category": "HW Info", + "_label": "IPMI Address" + }, + "ipv6address": { + "_label": "IPv6 Address", + "_required": "false" + }, + "is_virtual": { + "_label": "Virtual", + "_required": "false" + }, + "kernelrelease": { + "_fact": "kernelrelease", + "_flavor": "_fact", + "_format": "string", + "_label": "Kernel Release" + }, + "_key": "fqdn", + "memorysize": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Memory", + "_required": "false" + }, + "netdriver": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "string", + "_label": "Netdriver" + }, + "netdriver_duplex": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "string", + "_label": "Netdriver Duplex" + }, + "netdriver_firmware": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "string", + "_label": "Netdriver Firmware" + }, + "netdriver_speed": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "string", + "_label": "Netdriver Speed" + }, + "netdriver_version": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "string", + "_label": "Netdriver Version" + }, + "notes": { + "_field_category": "main", + "_format": "text", + "_label": "Notes", + "_required": "false" + }, + "operatingsystem": { + "_enumeration": { + "attribute": "operating_system", + "entity": "system", + "enumerator": [ + "Debian", + "RedHat" + ], + "forceselect": "false" + }, + "_fact": "operatingsystem", + "_flavor": "config", + "_format": "string", + "_label": "OS" + }, + "operatingsystemrelease": { + "_fact": "operatingsystemrelease", + "_flavor": "config", + "_format": "string", + "_label": "OS Release" + }, + "physical_processor_count": { + "_field_category": "HW Info", + "_label": "Processor Count", + "_required": "false" + }, + "power_consumption_avg": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "int", + "_label": "Average Power Consumption(Watts)", + "_validation": { + "is_int": {} + } + }, + "power_consumption_peak": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "int", + "_label": "Peak Power Consumption(Watts)", + "_validation": { + "is_int": {} + } + }, + "power_consumption_peak_time": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "int_timestamp", + "_label": "Time of Peak Power (Unix Timestamp, UTC)" + }, + "power_supply_watts": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_format": "float", + "_label": "Power Supply(Watts)", + "_validation": { + "is_float": {} + } + }, + "raidbaddrives": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Raid Bad Drives", + "_required": "false" + }, + "raidcontroller": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Raid Controller", + "_required": "false" + }, + "raiddrives": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Raid Drives", + "_required": "false" + }, + "raiddrivestatus": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Raid Drive Status", + "_required": "false" + }, + "raidtype": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Raid Type", + "_required": "false" + }, + "raidvolumes": { + "_field_category": "HW Info", + "_flavor": "_fact", + "_label": "Raid Volumes", + "_required": "false" + }, + "role_version": { + "_fact": "notestore_version", + "field_catetory": "main", + "_label": "Role Version" + }, + "roles": { + "_enumeration": { + "attribute": "role_id", + "entity": "role", + "forceselect": "true" + }, + "_field_category": "main", + "_format": "multiselect", + "_label": "Roles" + }, + "size": { + "_label": "Instance Size", + "_meta": "true", + "_required": "false" + }, + "svc_id": { + "_enumeration": { + "attribute": "name", + "entity": "service_instance", + "enumerator": [ + "cmdb", + "ldap_ba1", + "testsvc" + ] + }, + "_field_category": "main", + "_label": "Service Record", + "_required": "false" + }, + "tags": { + "_label": "Tags", + "_required": "false" + }, + "uuid": { + "_flavor": "fact", + "_label": "UUID" + }, + "virtual": { + "_flavor": "_fact", + "_label": "VM Type", + "_required": "false" + }, + "warranty_info": { + "_field_category": "HW Info", + "_format": "text", + "_label": "Warranty Info", + "_required": "false" + } + }, + "user": { + "_field_order": "username,systemuser,groups,writeaccess,sshkey", + "groups": { + "_enumeration": { + "enumerator": [ + "OPS", + "IT", + "QA" + ] + }, + "_format": "array", + "_label": "Groups" + }, + "_key": "username", + "sshkey": { + "_format": "text", + "_label": "SSH Key" + }, + "systemuser": { + "_format": "bool", + "_label": "System User" + }, + "username": { + "_format": "string", + "_label": "Username" + }, + "writeaccess": { + "_format": "bool", + "_label": "Write Access" + } + } + } +} diff --git a/log4perl.conf b/log4perl.conf index 1eed5ef..d8fb8aa 100644 --- a/log4perl.conf +++ b/log4perl.conf @@ -15,10 +15,22 @@ ############ #####/etc/log4perl.conf############################### -#log4perl.logger.inventory.cmdb_api = WARN, FileAppndr1 -log4perl.logger.inventory.cmdb_api = DEBUG, FileAppndr1 +log4perl.logger.inventory.cmdb_api = WARN, FileAppndr1 log4perl.appender.FileAppndr1 = Log::Log4perl::Appender::File -log4perl.appender.FileAppndr1.filename = /var/log/httpd/cmdb_api.log -log4perl.appender.FileAppndr1.layout = Log::Log4perl::Layout::SimpleLayout +log4perl.appender.FileAppndr1.filename = /var/log/apache2/cmdb_api.log +#log4perl.appender.FileAppndr1.layout = Log::Log4perl::Layout::SimpleLayout +log4perl.appender.FileAppndr1.layout = Log::Log4perl::Layout::PatternLayout +log4perl.appender.FileAppndr1.layout.ConversionPattern=%d %p:%P> %F{1}:%L %M - %m%n ###################################################### + +############################################################ +# Log::Log4perl conf - Syslog # +############################################################ +log4perl.logger.inventory.syslog = WARN, syslog +log4perl.appender.syslog = Log::Dispatch::Syslog +log4perl.appender.syslog.min_level = debug +log4perl.appender.syslog.ident = cmdb_api +log4perl.appender.syslog.facility = daemon +log4perl.appender.syslog.layout = Log::Log4perl::Layout::PatternLayout +log4perl.appender.syslog.layout.ConversionPattern=PID:%P %F{1}:%L %M - %m%n diff --git a/pp_lexicon.xml b/pp_lexicon.xml deleted file mode 100644 index 3b26634..0000000 --- a/pp_lexicon.xml +++ /dev/null @@ -1,877 +0,0 @@ - - - - - - - - - - config - true - string - - - - config - string - fqdn - - - - config - string - false - - - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - - - config - true - string - - - - config - true - string - - - - config - true - string - - - - config - true - string - - inbound - outbound - - - - - config - true - string - - - - - - config - true - string - - - - config - false - string - - - - config - true - string - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - true - string - - - - config - false - string - - - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - true - string - - - - config - false - string - - - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - false - string - - - - config - true - string - - - - config - false - string - - - - - - fact - true - string - - - - - - - - - - - - config - string - true - - - - config - string - true - - - - config - string - false - - - - config - string - false - - - - - config - text - false - - - - string - - - - string - - - - - - - - - - - - - - - - - - - - - - - - config - string - fqdn - - - - config - string - - blade_chassis - console_server - firewall - load_balancer - network_switch - other - power_strip - router - rsa_appliance - storage_head - storage_shelf - system - vpn - - - - - - - - - string - serial_number - - - - config - string - - POD - Archiving - Core - EDN - Eng - - - - - - - - - string - - /.*/ - - - - - fact - string - - /.*/ - - - - - fact - true - string - - - - - - - - - - - - fact - string - - - 42 - - - - - - string - manufacturer - - - - - - string - product_name - - - - - - - - - - - false - config - string - - production - deployment - allocated - idle - in transit - degraded - decommissioned - disposed - - - - - - - - - - - - - - - - string - - - - config - string - operatingsystem - - - - config - string - operatingsystemrelease - - - - - - - string - - - - config - string - operatingsystem - - - - config - string - operatingsystemrelease - - - - - - - - string - - - - config - string - operatingsystem - - - - config - string - operatingsystemrelease - - - - fact - float - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - config - string - - - - - - - - - - - - - - - - - - - - - - - - - - - config - string - operatingsystem - - - - - config - string - operatingsystemrelease - - - - fact - string - kernel_release - - - - false - string - - - - fact - string - - - - fact - string - - - - fact - string - - - - fact - string - - - - fact - string - - - - - - - multiselect - - - - - - - - - fact - float - - - - - - - fact - int - - - - - - - fact - int - - - - - - - fact - int_timestamp - - - - - false - fact - string - - - - false - fact - string - - - - - - - - - - - - false - config - string - - - - - - - - - - - - - - - - - - - - - - - - - true - int - - - - true - string - - - - true - string - - - - true - string - - - - true - string - - - - - - - - - - - - - - - - - OPS - IT - QA - - - - - - - - - - - - - - - - - - - - - - - - - - fact - string - manufacturer - - - - - string - product_name - - - - - - fact - float - - - - - - - int - false - - - - - - - - - diff --git a/readme.txt b/readme.txt index 663c9fc..c75b0c3 100644 --- a/readme.txt +++ b/readme.txt @@ -1,3 +1,8 @@ +testing with cmdb_test.pl +-the existing test in cmdb_tests.csv depend on a 'readonly' user being setup in a 'readonly' group. otherwise acl tests will fail +-these tests also depend on the lexicon having a 'ipmi_ip' field and the config item 'traffic_control_search_fields' containing + the 'ipaddress' field + new dependencies: -log4perl -perl-DateManip