# # Copyright (c) 2001-2016 NetApp, Inc., All Rights Reserved # Any use, modification, or distribution is prohibited # without prior written consent from NetApp, Inc. # ## @summary Represents an ONTAP system ## @author dl-nacl-dev@netapp.com ## @status shared ## @pod here =head1 NAME NACL::CS::ComponentState::ONTAP =head1 SYNOPSIS use base 'NACL::CS::ComponentState::ONTAP'; =head1 DESCRIPTION NACL::CS::ComponentState::ONTAP is an abstract base class for all ONTAP Component state libraries. It is derived from L and it provides ONTAP specific component base class methods. The implementation and documentation of provided functionality is present in L. =head1 ATTRIBUTES Same as those of L (command interfaces are constrained to be sub-classes of C.) =cut package NACL::CS::ComponentState::ONTAP; use strict; use warnings; use feature 'state'; use base 'NACL::CS::ComponentState'; use List::Compare; use List::Util; use Class::MethodMaker [ scalar => [ { -type => 'NACL::C::CommandInterface::ONTAP' }, 'command_interface', ], ]; use NACL::APISet::Exceptions::NoMatchingEntriesException qw(:try); use NATE::Exceptions::Argument; use NACL::APISet::Exceptions::InvalidParamException (); use NACL::Exceptions::InvalidFilterField (); use NACL::APISet::Exceptions::CommandFailedException; use NACL::Exceptions::NoElementsFound; use NATE::ParamSet qw(param_global); use Params::Validate qw(:all); use NACL::ComponentUtils qw( Dumper _ontap_ci_validate_spec _get_required_filter_fields ); use NACL::Realm; use NATE::Log qw(log_global); my $Log = log_global(); my $may_enter = $Log->may_enter(); my $may_exit = $Log->may_exit(); =head2 _fetch_cmode_cli_backend_validate_spec # NACL/CS/ComponentState.pm sub _fetch_cmode_cli { my $pkg = shift; my %opts = validate @_, $pkg->_fetch_cmode_cli_backend_validate_spec(); ... } The Params::Validate spec describing the arguments available in the CMode CLI backend implementations for "fetch". As L<_fetch_backend_validate_spec|lib-NACL-CS-ComponentState-pm/_fetch_backend_validate_spec> but also includes "api", which is a string which specifies the API to be invoked on the APISet object. =cut sub _fetch_cmode_cli_backend_validate_spec { my $pkg = shift; my $validate_spec = $pkg->_fetch_backend_validate_spec(); $validate_spec->{api} = { type => SCALAR }; $validate_spec->{'connectrec-max_idle'} = { type => SCALAR, default => 300 }; $validate_spec->{is_singleton} = { type => SCALAR | UNDEF, optional => 1 }; $validate_spec->{test_fields} = { type => ARRAYREF, optional => 1 }; $validate_spec->{response_exception_ref} = { type => SCALARREF, optional => 1 }; $validate_spec->{sort_by} = { type => ARRAYREF, optional => 1 }; return $validate_spec; } =head2 _fetch_cmode_gui_backend_validate_spec # NACL/CS/ComponentState.pm sub _fetch_cmode_gui { my $pkg = shift; my %opts = validate @_, $pkg->_fetch_cmode_gui_backend_validate_spec(); ... } The Params::Validate spec describing the arguments available in the CMode GUI backend implementations for "fetch". As L<_fetch_backend_validate_spec|lib-NACL-CS-ComponentState-pm/_fetch_backend_validate_spec> but also includes "api", which is a string which specifies the API to be invoked on the APISet object. =cut sub _fetch_cmode_gui_backend_validate_spec { my $pkg = shift; my $validate_spec = $pkg->_fetch_backend_validate_spec(); $validate_spec->{api} = { type => SCALAR }; $validate_spec->{copy} = { type => ARRAYREF|UNDEF }; $validate_spec->{map} = { type => HASHREF|UNDEF }; $validate_spec->{_primary_keys} = { type => ARRAYREF }; $validate_spec->{is_singleton} = { type => SCALAR | UNDEF, optional => 1 }; $validate_spec->{response_exception_ref} = { type => SCALARREF, optional => 1 }; return $validate_spec; } =head2 _compute_cmode_cli_options sub _fetch_cmode_cli { my $pkg = shift; ... my $api_opts = $pkg->_compute_cmode_cli_options( filter => $opts{filter}, requested_fields => $opts{requested_fields}, command_interface => $opts{command_interface} ); ... $apiset->$api(%$api_opts); ... } This method uses the filter and the requested fields provided to construct the options to be sent to the CMode CLI command to filter as per the rules defined by "filter" and to show only the fields specified by "requested_fields". Though it is possible to run the command without this filtering performed and then perform it on the output obtained, it is cleaner (i.e. leads to more readable and understandable logs) to let the command do the filtering for us. In addition to filter and requested fields, the command_interface argument also needs to be passed. This is so that for all components whose _primary_keys_validate_spec returns various specific specs based on the command_interface passed in, we will get the most accurate primary keys spec. This is applicable only for CMode CLI, since all CMode CLI commands provide the facility to filter according to these rules. =cut sub _compute_cmode_cli_options { my ( $pkg, @args ) = @_; my %opts = validate @args, { filter => { type => HASHREF }, requested_fields => { type => ARRAYREF | UNDEF }, command_interface => { type => OBJECT, isa => 'NACL::C::CommandInterface::ONTAP' }, is_singleton => {type => SCALAR|UNDEF, default => 0}, sort_by => { type => ARRAYREF | UNDEF }, }; my $command_interface = $opts{command_interface}; my %options; if (!$opts{is_singleton}) { %options = %{$opts{filter}}; # "filter" applicable only if is_singleton is 0 foreach my $key ( keys %options ) { if ( ref $options{$key} eq 'ARRAY' ) { $options{$key} = join ',', @{ $options{$key} }; } } } my $requested_fields = $opts{requested_fields}; my $sort_by_fields = $opts{sort_by}; # If a vserver is used as command_interface, then we are in vserver # context. From FS.0, "vserver" cannot be provided as input to any # command. Here we delete vserver from requested_fields and filter # if present and if in vserver context. if ($command_interface->isa('NACL::C::Vserver')) { my $vs_ci = $command_interface->vserver(); my $vs_filter = $options{vserver}; # Remove "vserver" if provided in the filter, if in vserver context # and the two match. If they do not match, do NOT remove it so that # it gets passed through to the command and fails there. if (defined $vs_filter && $vs_ci eq $vs_filter) { delete $options{vserver}; } # Delete vserver from requested_fields my %req_field_hash = map { $_ => 1 } @$requested_fields; delete $req_field_hash{vserver}; $requested_fields = [keys %req_field_hash]; } # If requested_fields is undef, or sent as [], then there is no extra # processing to do. if ( $pkg->_want_all_fields($requested_fields) ) { # Nothing to be done here } else { my $dummy; if ( $pkg->_can_C_file_be_loaded( error_message_ptr => \$dummy ) ) { # If the C file can be loaded, the original caller might be # find. find() sends all of the primary keys as the # requested_fields. This can be a problem for cases # where different sets of keys (but not all keys) are applicable # for different versions. Hence, we cannot have "-fields" # contain all the requested_fields. Instead, we make it contain # the non-optional (i.e. valid in all versions/interfaces/modes) # primary keys and any other fields sent in. The product # always shows the primary keys applicable to it even if # not all the primary keys are provided in "-fields". my $C_package_name = $pkg->get_C_package_name(); my @fields; my %primary_keys_spec = $C_package_name->_primary_keys_validate_spec( command_interface => $opts{command_interface} ); foreach my $requested_field ( @{$requested_fields} ) { unless ( exists $primary_keys_spec{$requested_field} && $primary_keys_spec{$requested_field}->{optional} ) { push @fields, $requested_field; } } my $field_str = join ',', @fields; # The "fields" option should only be present if $field_str is # not ''. Refer to burt 460306 $options{fields} = $field_str if ( length $field_str ); } else { # The C file cannot be loaded, hence the original caller # was for a CS-like file (i.e. a CS representation for a # command that returns output useful to the user). In this # case we just need to join the requested_fields together # on comma and send that as "-fields" my $field_str = join ',', @{$requested_fields}; $options{fields} = $field_str if ( length $field_str ); } } if ( scalar @{$sort_by_fields} ){ my $field_str = join ',', @{$sort_by_fields}; $options{'sort-by'} = $field_str if ( length $field_str ); } return \%options; } ## end sub _compute_cmode_cli_options =head2 _throw_cmode_cli_invalid_filter sub _fetch_cmode_cli { my $pkg = shift; ... try { $apiset->foo(%$opts); ... } catch NACL::APISet::Exceptions::NoMatchingEntriesException with { } catch NACL::APISet::Exceptions::InvalidParamException with { $pkg->_throw_cmode_cli_invalid_filter(@_); }; ... } If any of the filter fields or any of the requested fields are invalid, then the CMode CLI APISet throws an "InvalidParamException". This turns the "InvalidParamException" into an "InvalidFilterField" exception, using the exception text to determine whether an invalid filter or requested field was provided. =cut sub _throw_cmode_cli_invalid_filter { my ( $pkg, $exception ) = @_; my $exception_text = $exception->text(); $exception_text =~ s/invalid\sargument\s(.*)/$1/; my $error_text; if ( $exception_text =~ /\-/ ) { $error_text = "Invalid filter field $exception_text provided"; } else { $error_text = "Invalid requested field $exception_text provided"; } NACL::Exceptions::InvalidFilterField->throw($error_text); } =head2 _fetch_cmode_cli # NACL/CS/Foo.pm sub _fetch_cmode_cli { my $pkg = shift; return $pkg->SUPER::_fetch_cmode_cli( @_, api => $api); } This is the base implementation of _fetch_cmode_cli. The only information the implementation of _fetch_cmode_cli in the individual ComponentState modules need to provide is the name of the API to be invoked. The base implementation provided here computes the options to be passed to the API (uses L<_compute_cmode_cli_options>), invokes the API and constructs state objects out of it. It also throws an InvalidFilterField exception if one was provided (uses L<_throw_cmode_cli_invalid_filter>) =over =item Options In addition to the options passed to NACL::CS::Foo::_fetch_cmode_cli, the following options can be passed =over =item C<< api => $api_to_invoke >> (Required) This is the API to be invoked to get the state details. (generally the relevant 'show' command) =item C<< is_singleton => $boolean >> (Optional) Certain tables are implemented as singletons, i.e. only one row of output can show up. These commands do not support filtering on fields (which all other 'show' commands provide). This option should be specified (and value should be 1) for such singleton commands. =item C<< connectrec-max_idle => $seconds >> (Optional) If this option is given and non-zero, and if the command does not produce some output for a period of more than this many seconds, throw an exception. =item C<< test_fields => [ $field1, field2, ...] >> (Optional, ARRAYREF) Certain fields are accessible only in test privilege. Through this option we can specify these fields. If one (or more) of these fields is requested for, the API will be invoked in test privilege. =back =back =cut sub _fetch_cmode_cli { $Log->enter() if $may_enter; my $pkg = shift; my %opts = validate @_, $pkg->_fetch_cmode_cli_backend_validate_spec(); my $apiset = $opts{apiset}; my $api = $opts{api}; my $requested_fields = $opts{requested_fields}; my $filter = $opts{filter}; my $test_fields = $opts{test_fields}; my $command_interface = $opts{command_interface}; my $response_exception_ref = delete $opts{response_exception_ref}; # If it came through here, then don't call _apply_filter. However, # we do need to apply the filter for singleton tables ${ $opts{_apply_filter} } = 0 unless ( $opts{is_singleton} ); # The "applicable" filter contains only those fields listed in the # CDEF, i.e. it only has the fields that should be sent to the command. my %applicable_filter = %$filter; # We check for whether the data-type of the filter fields are valid. # For this, we divide the filter into two: those fields which are # currently defined in MethodMaker ("orig_filter") and those fields which # will get dynamically added after the API call ("other_filter"). The # other_filter gets validated after the API call. my %orig_filter = %applicable_filter; my %other_filter; my %applicable_req_field_hash; foreach my $req_field (@$requested_fields) { $applicable_req_field_hash{$req_field} = 1; } my ( @underscore_warnings, @invalid, %options ); my $cdef = $apiset->get_command_definition(command => $api); my $alias_map = $cdef->get_alias_map(); my @options = $apiset->get_allowed_options_for_command(command => $api, cdef_obj => $cdef); my @alias = (); if ( $alias_map ) { @alias = keys %{ $alias_map }; } map { $options{$_} = 1 } ( @options, @alias ); # This code-ref iterates over the fields of the filter and # requested_fields and does the following: # 1. Checks if a field is invalid (a field is considered to be invalid # if it is not defined in MethodMaker and it is not present in the CDEF) # 2. If it is defined in MethodMaker but is not present in the CDEF file, # then it is removed from the "applicable" filter/requested_fields. # (the "applicable" fields are those present in the CDEFs and which # should be sent down to the command). Further, a check is performed # to check if a field that should have been hyphenated was provided # in the underscored format, and if so a warning is generated. # 3. If a field is not defined in MethodMaker but is present in the # filter, then it is moved from the orig_filter to the other_filter. # # The purpose of the "is_filter" option is to denote whether we're # iterating over filter fields or requested_fields. Depending on which # one we're iterating over, which hash to delete from differs. my $check_for_valid_fields = sub { my %opts = validate_with( params => \@_, spec => { is_filter => { type => SCALAR }, fields => { type => ARRAYREF } } ); my $is_filter = $opts{is_filter}; foreach my $field ( @{ $opts{fields} } ) { if ( $pkg->can($field) ) { if ( !exists $options{$field} ) { if ($is_filter) { delete $applicable_filter{$field}; } else { delete $applicable_req_field_hash{$field}; } my $underscored_field = $field; $underscored_field =~ s/_/-/g; if ( exists $options{$underscored_field} ) { push @underscore_warnings, $field; } } } elsif ( exists $options{$field} ) { # Field currently not in MethodMaker but is listed in the CDEFs if ($is_filter) { $other_filter{$field} = delete $orig_filter{$field}; } } else { # Field not in MethodMaker or in the CDEFs, must be invalid push @invalid, $field; } } }; $check_for_valid_fields->( is_filter => 1, fields => [ keys %orig_filter ] ); $check_for_valid_fields->( is_filter => 0, fields => [ keys %applicable_req_field_hash ] ); if (@invalid) { my $error_msg = "Invalid requested or filter field(s) provided: " . join( ",", @invalid ); $Log->exit() if $may_exit; NACL::Exceptions::InvalidFilterField->throw($error_msg); } if (@underscore_warnings) { $Log->warn( "Fields provided in 'requested_fields' or 'filter' should " . 'be provided in their hyphenated format, but the following ' . "fields were provided in the underscored format:\n" . join( ', ', @underscore_warnings ) . "\nThese fields have been ignored since they're provided " . 'in the underscored format' ); } my $catch_exception_and_generate_warning = sub { my ($filter) = validate_pos( @_, { type => HASHREF } ); if ( keys %$filter ) { try { $pkg->_check_data_type_of_filter( filter => $filter ); } catch NATE::Exceptions::Argument with { my $exception = shift; $Log->warn( $exception->text() . '. Please update the call ' . 'to send in this field as the appropriate data-type ' . '(note that the current call will fail when run aganst ' . 'any other mode or interface)' ); }; } }; $catch_exception_and_generate_warning->( \%orig_filter ); if ( $alias_map ) { foreach my $alias_field ( keys %{ $alias_map } ) { if ( $applicable_filter{$alias_field} ) { $applicable_filter{$alias_map->{$alias_field}} = delete $applicable_filter{$alias_field}; } } } my $api_opts = $pkg->_compute_cmode_cli_options( filter => \%applicable_filter, requested_fields => [ keys %applicable_req_field_hash ], command_interface => $command_interface, is_singleton => $opts{is_singleton}, sort_by => $opts{sort_by}, ); if ($command_interface->timeout_scaling_factor_isset() && $opts{'connectrec-max_idle'} != -1 ) { $api_opts->{'connectrec-max_idle'} = $opts{'connectrec-max_idle'} *$command_interface->timeout_scaling_factor(); } else { $api_opts->{'connectrec-max_idle'} = $opts{'connectrec-max_idle'}; } my @state_objs; my $retry_count = 1; AGAIN: { # The API call may throw an exception as a result of no entries matching # the query being found or because of an invalid filter field or invalid # requested field being passed. If no entries matching the query are # found, a "NoMatchingEntriesException" is thrown. We just catch it and # do nothing. The 'fetch' frontend decides whether to throw a # NoElementFound exception based on the value of allow_empty. # If an exception occured as a result of the other cases, then an # InvalidParamException is thrown, we catch this and turn it into # an InvalidFilterField exception (performed by the method # _throw_cmode_cli_invalid_filter). try { my $response = $apiset->$api( %{$api_opts}, cdef_obj => $cdef ); @state_objs = $pkg->_construct_cs_objs_cm_cli( 'response_object' => $response, 'command_interface' => $command_interface, 'api' => $api ); } catch NACL::APISet::Exceptions::NoMatchingEntriesException with { my $ex = shift; $Log->debug('No entries found matching the query'); $$response_exception_ref = $ex if ( defined $response_exception_ref ); } catch NACL::APISet::Exceptions::InvalidParamValueException with { my $ex = shift; $Log->debug( "Caught InvalidParamValueException and the error is:\n" . $ex->text() ); $$response_exception_ref = $ex if ( defined $response_exception_ref ); if ( $ex->text !~ /invalid value for field \"\-vserver/ ) { # Pretty much, CM CLI says "No matching entries ..." when # specifying an invalid filter for all other fields except # 'vserver'. On specifying invalid value for 'vserver', it # complains 'invalid value for field "-vserver ... '. Hence # handling it to give the same behavior for ZAPI and CLI. # If we get this exception for any other fields, I'm not sure what # the case is and so I'm rethrowing the exception for now and # handle that case when we hit it. $Log->debug("Rethrowing the exception"); $Log->exit() if $may_exit; $ex->throw(); } } catch NACL::APISet::Exceptions::InvalidParamException with { my $ex = $_[0]; $$response_exception_ref = $ex if ( defined $response_exception_ref ); $Log->exit() if $may_exit; $pkg->_throw_cmode_cli_invalid_filter(@_); } catch NATE::BaseException with { my $ex = shift; if($ex =~ /perl identifier/ && $retry_count > 0){ $retry_count--; redo AGAIN; } $ex->throw(); }; } #AGAIN if ( !$opts{show_cmd} ) { try { $pkg->SUPER::_check_for_invalid_requested_filter_fields(requested_fields => $requested_fields, filter => $filter); } catch NACL::Exceptions::InvalidFilterField with { my $e = shift; my ( %allowed_options, @invalid_fields, @unknown_input_fields ); push(@unknown_input_fields, $e->unknown_requested_fields()); push(@unknown_input_fields, $e->unknown_filters()); my @allowed_options = $apiset->get_allowed_options_for_command( command => $api ); map { $allowed_options{$_} = 1 } @allowed_options; foreach my $field ( @unknown_input_fields ) { if ( !$allowed_options{$field} ) { push(@invalid_fields, $field); } } if ( @invalid_fields ) { my $err_str = 'Unknown field(s) '. join(",", @invalid_fields ).' This could be ' . "because:\n" . "\t1. The field is invalid\n" . "\t2. The field is valid but has " . "not been implemented on the CS object '$pkg'. If " . 'this is the case, please raise a burt against ' . 'nacl (type=nacl;subtype=nacl_core) or mail ' . 'dl-nacl-dev@netapp.com regarding the issue'; $Log->exit() if $may_exit; NACL::Exceptions::InvalidFilterField->throw($err_str); } }; } $catch_exception_and_generate_warning->( \%other_filter ); $Log->exit() if $may_exit; return @state_objs; } =head2 _fetch_cmode_gui # NACL/CS/Foo.pm sub _fetch_cmode_gui { my $pkg = shift; return $pkg->SUPER::_fetch_cmode_gui( @_, api => $api); } This is the base implementation of _fetch_cmode_gui. The only information the implementation of _fetch_cmode_gui in the individual ComponentState modules need to provide is the name of the API to be invoked. The base implementation provided here computes the options to be passed to the API (uses L<_compute_cmode_cli_options>), invokes the API and constructs state objects out of it. It also throws an InvalidFilterField exception if one was provided (uses L<_throw_cmode_cli_invalid_filter>) =over =item Options In addition to the options passed to NACL::CS::Foo::_fetch_cmode_gui, the following options can be passed =over =item C<< api => $api_to_invoke >> (Required) This is the API to be invoked to get the state details. =back =back =cut sub _fetch_cmode_gui { $Log->enter() if $may_enter; my $pkg = shift; my %opts = validate @_, $pkg->_fetch_cmode_gui_backend_validate_spec(); my $apiset = $opts{apiset}; my $api = $opts{api}; my $requested_fields = $opts{requested_fields}; my $filter = $opts{filter}; my $command_interface = $opts{command_interface}; my $response_exception_ref = delete $opts{response_exception_ref}; my @state_objs; my $api_opts = $pkg->_get_required_filter_fields(%opts); $Log->debug( sub { "API options:\n" . Dumper($api_opts) } ); my $response; my $element_not_found = 0; try { $response = $apiset->$api( %{$api_opts} ); } catch NATE::BaseException with { my $exception = shift; if ($exception->text() =~ /not present/i) { $element_not_found = 1; } else { $Log->exit() if $may_exit; $exception->throw(); } }; if ($element_not_found) { $Log->exit() if $may_exit; return; } my $output = $response->get_parsed_output(); my %obj_fields; $pkg->_hash_copy( source => $output->[0], target => \%obj_fields, copy => $opts{copy}, map => $opts{map}, ); my $obj = $pkg->new(command_interface => $opts{command_interface}); $obj->_set_fields(row => \%obj_fields, need_to_quote => 0, is_zapi => 0); push @state_objs, $obj; $Log->exit() if $may_exit; return @state_objs; } =head2 _handle_zapi_vserver_context # NACL/C/Foo.pm sub _foo_cmode_zapi { ... my $command_interface = $opts{command_interface}; my $apiset = $opts{apiset}; my $vserver = $opts{vserver}; ... $pkg_or_obj->_hash_copy(...); my $vserver_operated_on = $pkg_or_obj->_handle_zapi_vserver_context( api_opts => \%cmode_args, vserver => $vserver, command_interface => $command_interface, frontend_method => $method ); $apiset->foo_api(%cmode_args); } (For component developers) Certain commands/ZAPIs can be run in Vserver context. These methods are used to handle the differences between running these methods using Vserver as a command interface or using Node/Cluster/Cserver as the command interface. It returns the vserver being operated on (could be the "vserver" argument passed in, or the name of the vserver which the Vserver command interface is modeling) For ZAPI, the method to be invoked is C<_handle_zapi_vserver_context>. This ensures that if a vserver-scope ZAPI is being invoked with a Node command interface, then it will set the API to run in the context of the vserver passed as argument. This method throws a C exception in the following cases: =over =item 1) If a vserver command interface is used and a vserver argument is sent in the method call, but the argument does not match the vserver of the command interface =item 2) The vserver argument is not passed when a non-Vserver command interface is passed =back This method returns the name of the vserver on which we will operate. (This could be the vserver attribute of the command interface or the vserver option passed in) =over =item Options =over =item api_opts (HASHREF, Mandatory) A reference to the hash sent to the API call. This gets updated to contain the vserver context to set to if a Node/Cluster command interface is used =item vserver (Mandatory) The vserver option passed in =item command_interface (Isa 'NACL::C::CommandInterface::ONTAP', Mandatory) The command interface being used =back =back =cut # Get implementations of _handle_zapi_vserver_context and # _handle_vserver_context from the mix-in use NACL::C::_Mixins::Component::ONTAP qw(:all); =head2 _construct_cs_objs_cm_cli sub _construct_cs_objs_cm_cli(reponse_object => $resp_obj ); This method is a helper method which will accept response object and constructs state objects. =cut sub _construct_cs_objs_cm_cli { $Log->enter() if $may_enter; my ($pkg, @args) = @_; state $spec = { response_object => { type => OBJECT }, command_interface => { isa => 'NACL::C::CommandInterface', type => OBJECT }, api => { type => SCALAR } }; my %opts = validate_with( params => \@args, spec => $spec, ); my $response = $opts{'response_object'}; my $output = $response->get_parsed_output(); if (!@$output) { $Log->exit() if $may_exit; return (); } my $command_interface = $opts{'command_interface'}; my $api = $opts{'api'}; my ($need_to_populate_vs, $vs_name); # From SN.0 onwards, in vserver context the "vserver" field # no longer shows up in the output of "show" commands. For # backwards compatibility, we populate its value using the # vserver name from the vserver command_interface. # Note: The ordering of conditions is such that the quickest conditions # are first in the list. if (!exists $output->[0]{vserver} && $command_interface->isa('NACL::C::Vserver') && $pkg->can('vserver')) { $need_to_populate_vs = 1; $vs_name = $command_interface->vserver(); } my %compatibility_hash = reverse %{$response->{output_compat_mirror}}; # Dynamically add any fields that show up in the parsed output but are # not defined in MethodMaker. my @compat_fields; my %new_fields_key_val; while (my ($key, $value) = each %{$output->[0]}) { if (!$pkg->can($key)) { # Field is not present in MethodMaker # Skip it if it's a compatibility field (therefore is an old field) if (exists $compatibility_hash{$key}) { # Maintain a list of the compatibility fields that show up # but are not in MethodMaker. We will later move them out # before the call to _set_fields() to prevent unnecessary # warnings showing up. push @compat_fields, $key; } else { $new_fields_key_val{$key} = $value; } } } my @new_fields = keys %new_fields_key_val; if (@new_fields) { # Keys are not present in MethodMaker or the compatibility list; # try to determine its data-type my $cmd_details = $pkg->_determine_command_details_multiple_fields(%opts, keys => \@new_fields); while (my ($key, $value) = each %new_fields_key_val) { my $type = $cmd_details->{$key}; my %install_args; if (defined $type) { $install_args{type} = $type; } else { $install_args{value} = $value; } eval { $pkg->_install_methodmaker_field(field => $key, %install_args); }; if ($@) { NATE::BaseException->throw($@); } } } my @state_objs; foreach my $row ( @{$output} ) { my $obj = $pkg->new(command_interface => $command_interface); if (@compat_fields) { $pkg->_hash_move( source => $row, target => {}, move => \@compat_fields ); } $obj->_set_fields(row => $row, is_zapi => 0, need_to_quote => 1, api => $opts{api}); # From SN.0 onwards, in vserver context the "vserver" field # no longer shows up in the output of "show" commands. For # backwards compatibility, we populate its value using the # vserver name from the vserver command_interface. $obj->vserver($vs_name) if ($need_to_populate_vs); push @state_objs, $obj; } $Log->exit() if $may_exit; return @state_objs; } =head2 _determine_command_details $self->_determine_command_details( api => $api, key => $key ); (For developers only) This method determines the data types of the various options accepted for a particular show command. This makes use of certain features offered by the CMode CLI UI. When a command is issued with a "?" following it, the usage of the command is displayed. All of the options which are list-fields will have a ", ..." in their description. For example: toaster::*> storage aggregate show ? [ -checksum | -disk | -instance | -options | -raid-info | -fields , ... ] [[-aggregate] ] Aggregate [ -uuid ] *UUID [ -size {[KB|MB|GB|TB|PB]} ] Size [ -usedsize {[KB|MB|GB|TB|PB]} ] Used Size [ -percent-used ] Used Percentage [ -availsize {[KB|MB|GB|TB|PB]} ] Available Size [ -state ] State [ -nodes , ... ] Nodes [ -mirror|-m [true] ] Mirror [ -diskcount ] Number Of Disks [ -chksumstyle ] Checksum Style [ -disklist|-d , ... ] Disks for First Plex [ -mirror-disklist , ... ] Disks for Mirrored Plex I, I and I are array-fields. =over =item Options =over =item C<< api => $api >> The API to be invoked =item C<< key => $key >> The field of the CS object. =back =back =cut sub _determine_command_details { $Log->enter() if $may_enter; my ($pkg_or_obj, @args) = @_; my %opts = validate_with( params => \@args, spec => { key => { type => SCALAR }, }, allow_extra => 1 ); my $key = delete $opts{key}; my $details_hash = $pkg_or_obj->_determine_command_details_multiple_fields( %opts, keys => [$key]); $Log->exit() if $may_exit; return $details_hash->{$key}; } sub _determine_command_details_multiple_fields { $Log->enter() if $may_enter; my ($pkg_or_obj, @args) = @_; my %opts = validate_with( params => \@args, spec => { api => { type => SCALAR }, keys => { type => ARRAYREF }, }, allow_extra => 1 ); my $api = $opts{api}; my $keys = $opts{keys}; my $ci = $opts{command_interface} // $pkg_or_obj->command_interface(); my $help_obj = NACL::Realm->realm()->get_help_xml_obj(command_interface => $ci); my $cmd_ref; if ($help_obj->help_xml_accessible()) { my $apiset = $ci->apiset(interface => 'CLI', set => 'CMode'); my $cdef = $apiset->get_command_definition(command => $api); my $cmd = $cdef->get_command(); my $alias_map = $cdef->get_alias_map(); $cmd_ref = $help_obj->get_attribute_map(command => $cmd, alias_map => $alias_map); } else { $cmd_ref = $help_obj->get_ui_details(api => $api, command_interface => $ci); } my $fields = $cmd_ref->{fields}; my $cmd_details_hash = {}; if (defined $fields) { foreach my $key (@$keys) { if (exists $fields->{$key}) { my $type = $fields->{$key}{is_array} ? 'array' : 'scalar'; $cmd_details_hash->{$key} = $type; } } } $Log->exit() if $may_exit; return $cmd_details_hash; } =head2 _reconcile_scalar_array_mismatch sub _set_fields { ... $self->_reconcile_scalar_array_mismatch( api => $opts{api}, key => $key, value => $value ); ... } (For developers only) If there is a mismatch between the type of the parsed output for a field and the MethodMaker definition in the CS file, this method will attempt to figure out why there is a mismatch (either the type of parsed output is wrong or the MethodMaker definition for that field is wrong). This is possible only for CMode. (This method does nothing for non-CMode command interfaces) See L<_determine_command_details|lib-NACL-CS-ComponentState-pm/_determine_command_details> for details of how the types of fields are determined. This method ensures that a warning for a particular field is only emitted once and also ensures that a help command is only invoked once for each command. The way it does so is as follows: =over =item * Check the %Warnings_Already_Thrown hash to see if the warning for this has already been emitted. If so, return immediately. =item * Else call _determine_command_details to determine the details of the fields of the command. =back In all cases, if the mismatch is found to be because the APISet parser returned the parsed output in the wrong format, we work around it by joining it back together into a scalar so that when accessing through the CS object we will correctly get a scalar value. =over =item Options =over =item C<< api => $api >> The API to be invoked =item C<< key => $key >> The field of the CS object. =item C<< value => $value >> The value for this field. =back =back =cut # Contains entries for each of the commands/options for which warnings have # already been emitted. For example: # $Warnings_Already_Thrown{$command}{$field} = 1 # means a warning has already been emitted for field $field of command $command my %Warnings_Already_Thrown; # In all cases, the value is set for that key. sub _reconcile_scalar_array_mismatch { my $self = shift; my %opts = validate( @_, { api => { type => SCALAR | UNDEF, optional => 1 }, key => { type => SCALAR }, value => { type => ARRAYREF } } ); my $api = $opts{api}; my $key = $opts{key}; my $value = $opts{value}; my $ref = ref $self; my $raise_burt_str = 'Please raise a burt against nacl ' . '(type=nacl;subtype=nacl_core) or mail ' . 'dl-nacl-dev@netapp.com regarding the issue'; my $field_type_could_not_be_determined = sub { $Log->warn( "The value for the field '$key' is an array " . "but it has been defined in $ref as a scalar " . 'field. This could either be because the APISet parser ' . 'has wrongly returned the value as an array-ref or ' . "because the field definition in $ref for '$key' is " . "wrong. $raise_burt_str" ); }; # By default, set the value for $key to $value. If we need to turn # the value back into a scalar, then that will be done below $self->$key($value); unless ( $self->command_interface_isset() && $self->command_interface()->is_cmode() && defined $opts{api} ) { $field_type_could_not_be_determined->(); $Log->exit() if $may_exit; return; } my $type = $self->_determine_command_details(%opts); my $join_into_scalar_and_set_value = sub { # Workaround the bug in the parser by joining it into a scalar my $scalar_value = join ', ', @$value; $self->$key($scalar_value); }; if ( exists $Warnings_Already_Thrown{$api}{$key} ) { if (defined $type && $type eq 'scalar') { $join_into_scalar_and_set_value->(); } } else { if (defined $type) { $Warnings_Already_Thrown{$api}{$key} = 1; if ($type eq 'array' ) { $Log->warn( "The value for the field '$key' is an array " . "but it has been wrongly defined in $ref as a " . "scalar field. $raise_burt_str" ); } else { $Log->warn( "The field '$key' is defined in $ref as a scalar " . 'but the APISet parser has wrongly returned it as an ' . "array-ref. $raise_burt_str" ); $join_into_scalar_and_set_value->(); } } else { $field_type_could_not_be_determined->(); } } $Log->exit() if $may_exit; } # Refer to the POD of NACL::CS::ComponentState for the description # of this method. This method fixes the value of node => 'local' # in the filter to node => $local_nodename for 7Mode. sub _apply_filter { $Log->enter() if $may_enter; my ( $pkg, @args ) = @_; my %opts = validate_with( params => \@args, spec => { state_objs => { type => ARRAYREF }, filter => { type => HASHREF }, }, allow_extra => 1 ); my $cs_obj = $opts{state_objs}->[0]; $pkg->_check_if_each_of_correct_type( state_objs => $opts{state_objs} ) if(defined($opts{state_objs}->[0])); if ( exists $opts{filter}{node} && defined $opts{filter}{node} && $opts{filter}{node} eq 'local' && defined $cs_obj && $cs_obj->can('node') ) { # Inspect one of the CS objects and see if the CS class defines # an attribute called 'node'. If so, during filtering, 'local' # should be meant as the name of the local node. This code is # expected to hit for 7M CLI, CM ZAPI which invokes non-iter ZAPIs if ( $cs_obj->command_interface_isset ) { $opts{filter}{node} = $cs_obj->command_interface->hostrec->hostname; } } my @filter_objs = $pkg->SUPER::_apply_filter(%opts); $Log->exit; return @filter_objs; } sub base_class { return 'NACL::CS::ComponentState::ONTAP'; } # If the value contains '*', '|', '..' or spaces and is not contained within # quotes, then it needs to be quoted. # For example, a qtree can be created with name "ab*". Unless the value # is quoted, then a query for this will show all qtrees whose name # begins with "ab", rather than querying for the individual qtree named # "ab*". # The test_special_characters test-case in Component.thpl has more details. # See https://library.netapp.com/ecm/ecm_get_file/ECMP1136871 # See burt 666005 (change 2030579). sub _needs_to_be_quoted { $Log->enter() if $may_enter; my ($pkg_or_obj, @args) = @_; my %opts = validate_with( params => \@args, # Do not validate this as type SCALAR. The value might be of some # other type (like a hash-reference or object). spec => { value => { required => 1 } } ); my $value = $opts{value}; my $ret = 0; # $value might be a hash-reference or object, in which case it obviously # doesn't need to be quoted. We perform the check below only if the value # provided is a scalar. # Note: the /s modifier is needed below since the value can be split over # multiple lines and without \s, "." does not consider new-line # characters. See the "handle_quoting" test-case in ComponentState.thpl. if (defined $value && !ref $value && ($value !~ /^".*"$/s && ($value =~ /[*|\s]/ || $value =~ /\S+\.\.\S+/))) { $ret = 1; } $Log->exit() if $may_exit; return $ret; } # Quoting needed only for CMode sub _need_to_check_quoting { $Log->enter() if $may_enter; my $self = $_[0]; my $ret = 1; if ($self->command_interface_isset()) { $ret = $self->command_interface()->is_cmode(); } $Log->exit() if $may_exit; return $ret; } ## base class to run 'get' for non-iter zapi sub _fetch_cmode_zapi_non_iter { $Log->enter() if $may_enter; my ($pkg, @args) = @_; my $spec = { %{$pkg->_fetch_backend_validate_spec()}, copy => { type => ARRAYREF, default => [] }, map => { type => HASHREF, default => {} }, primary_keys => { type => ARRAYREF }, api => { type => SCALAR }, vserver_operated_on => {type => SCALARREF, optional => 1}, inapplicable_for_help_xml => {type => ARRAYREF, default => []}, skip_auto_translation => {type => SCALAR , default => 0} }; my %opts = validate_with( params => \@args, spec => $spec, ); my $command_interface = $opts{command_interface}; if ( $opts{show_cmd} && $command_interface->can_get_version_details() ) { $pkg->_update_cli_zapi_mapping(\%opts); } my $copy = $opts{copy}; my $map = $opts{map}; my $primary_keys = $opts{primary_keys}; my $api = $opts{api}; my $vs_context = $opts{vserver_operated_on}; my $apiset = $opts{apiset}; my $skip_auto_translate = delete $opts{skip_auto_translation}; my %api_opts; if (defined $vs_context) { $$vs_context = $pkg->_handle_zapi_vserver_context( api_opts => \%api_opts, vserver => $opts{filter}->{vserver}, command_interface => $command_interface, ); } my @cli_to_zapi = %$map; # Only the primary keys are accepted by the get ZAPIs my %primary_keys_hash; if (@$primary_keys) { # _zapi_hash_copy complains if copy and map are empty arrays, so first # checks if the $primary_keys array-ref contains any entries. (This is # necessary to handle cases where there are no primary keys.) $pkg->_zapi_hash_copy( source => $opts{filter}, target => \%primary_keys_hash, copy => $primary_keys, ); } # Translate to ZAPI names by using map/copy $pkg->_zapi_hash_copy( source => \%primary_keys_hash, target => \%api_opts, copy => $copy, map => \@cli_to_zapi, target_skips => 1, ); # The fields in desired-attributes should be a union of what's in # requested_fields and the filter. However, if requested_fields is [], # then we need to get all fields, so leave it as-is. my (%source, %desired_attributes); my @req_field_filter_union = @{$opts{requested_fields}}; if (@req_field_filter_union) { push @req_field_filter_union, (keys %{$opts{filter}}); } # requested_fields maps to "desired-attributes" for the ZAPI. # Array-fields require the desire-attributes entry to have an empty # string with an arrayref, for scalar fields the value should just be # an empty string. foreach my $req_field (@req_field_filter_union) { $source{$req_field} = (ref $pkg->$req_field() eq 'ARRAY') ? [''] : ''; } $pkg->_zapi_hash_copy( source => \%source, target => \%desired_attributes, copy => $copy, map => \@cli_to_zapi, target_skips => 1, ); if (%desired_attributes) { $api_opts{'desired-attributes'} = \%desired_attributes; } my $response; try { $response = $apiset->$api(%api_opts); } catch NACL::APISet::Exceptions::NoMatchingEntriesException with { # NoMatchingEntries seems to be thrown by most of the non-iter ZAPIs. }; if (!$response) { # No matching entries, so return $Log->exit() if $may_exit; return; } my $output = $response->get_parsed_output(); # There's only one row of output returned. Using net-port-get as example, # the output structure would look like: # [ # { # attributes => # [ # { # 'net-port-info' => [ # { ... fields here ... } # ] # } # ] # } # ] # Stated generally, under output->[0]{attributes}[0] is a hash with # a single key. The actual entries are under this key. my $only_row = $output->[0]{attributes}[0]; # output->[0] might not have attributes like in below structure - # $VAR1 = { # 'persistent-reservation' => [ # { # 'persistent-reservation-info' => [ # { ... fields here ... } # ] # } # ] # }; if (! defined $only_row) { my $attribute = (keys $output->[0])[0]; $only_row = $output->[0]{$attribute}[0]; } my $key = (keys %$only_row)[0]; my $only_hashref = $only_row->{$key}[0]; # $map is CLI -> ZAPI, but we need to do the opposite mapping, so # reverse $map. my @zapi_to_cli = reverse @cli_to_zapi; my %obj_fields; $pkg->_zapi_hash_copy( source => $only_hashref, target => \%obj_fields, copy => $copy, map => \@zapi_to_cli, source_has_extra_arrays => 1, ); my $obj = $pkg->new(command_interface => $opts{command_interface}); $obj->_set_fields(row => \%obj_fields, need_to_quote => 1, is_zapi => 1); my @state_objs; # copy => [ qw(a b) ] is equivalent to doing map => {a => ['a'], b => ['b']} # For each element in copy, we make an entry of this form foreach ( @{$copy} ) { push @cli_to_zapi, ( $_ => [$_] ); } my %cli_to_zapi_hash = @cli_to_zapi; my %fields_that_should_be_filled_in = $pkg->_calculate_fields_to_be_filled_in( %opts, cli_to_zapi_hash => \%cli_to_zapi_hash); foreach my $key ( keys %fields_that_should_be_filled_in ) { my $isset = "${key}_isset"; if ( !$obj->$isset() ) { unless ($primary_keys_hash{$key}) { # If it is not a pri key, then fill it with hyphen $obj->$key('-'); } } } my %extra_opts = (); if($opts{show_cmd} && !$skip_auto_translate){ %extra_opts = ( show_cmd => $opts{show_cmd}, fields_that_should_be_filled_in => \%fields_that_should_be_filled_in, help_xml_obj => $opts{help_xml_obj}, zapi_field_translations => {}, ); } push @state_objs, $obj; $pkg->_update_state_objs_cmode_zapi( state_objs => \@state_objs, command_interface => $opts{command_interface}, ); $Log->exit() if $may_exit; return $obj; } sub _calculate_fields_to_be_filled_in { $Log->enter() if $may_enter; my ($pkg, @args) = @_; state $validate_spec = { requested_fields => {type => ARRAYREF, default => []}, cli_to_zapi_hash => {type => HASHREF}, }; my %opts = validate_with( params => \@args, spec => $validate_spec, allow_extra => 1, ); my %cli_to_zapi_hash = %{$opts{cli_to_zapi_hash}}; my %fields_that_should_be_filled_in; if ( !@{$opts{requested_fields}} ) { # Want all fields foreach my $key ( keys %cli_to_zapi_hash ) { $fields_that_should_be_filled_in{$key} = 1; } } else { my %req_field_hash; foreach my $req_field (@{$opts{requested_fields}}) { if ( exists $cli_to_zapi_hash{$req_field} ) { $fields_that_should_be_filled_in{$req_field} = 1; } } } # extra filter fields don't show up in the output my $extra_filter_fields = $pkg->_extra_filter_fields(); map {delete $fields_that_should_be_filled_in{$_}} @$extra_filter_fields; $Log->exit() if $may_exit; return %fields_that_should_be_filled_in; } # # Base class implementation that handles known translations. The known # translations are: # 1. add_percentage: CLI returns the value with a % at the end, ZAPI does not. # So add the % to the end of the value. # 2. hyphenate_value: ZAPI returns all values as underscored, but the # equivalent CLI value is hyphenated, so hyphenate the value. # 3. on_off_boolean: ZAPI returns the value as true/false; CLI returns as on/off. # 4. ucfirst: ZAPI returns the value in all lower-case, CLI value begins with # upper-case. # 5. timestamp_to_string: Convert a unix timestamp to a CLI time-string. # time-string will be of the form "Thu Mar 27 01:47:52 2014" # 6. uc: Upper-case the entire value. # 7. enable_disable: ZAPI returns the value as true/false; CLI returns as # enable/disable. # 8. timestamp_to_mmddyyyy: convert unix time to the form "MM/DD/YYYY HH:MM:SS". # 9. timestamp_to_mmdd: convert unix time to the form "MM/DD HH:MM:SS". # 10. sec_to_day_hrminsec: convert seconds to [d][h][m][s]. # 11. sec_to_hrminsec: convert seconds to [h][m][s]. # 12. decimal_to_hex: convert decimal number to hexadecimal number. # 13. yes_no_boolean: ZAPI returns the value as true/false; CLI returns as yes/no. # 14. integer_cron_time: ZAPI returns -1 for all the cron time while CLI returns 'all' # 15. unix_perm_to_str: ZAPI returns a numeric value (like 755), but the CLI # returns a string like "---rwxr-xr-x" # 16. language: ZAPI returns the value as all lower-case, with all underscores, # but the CLI returns a mix of upper/lower case and underscores/hyphens. # # The call in the individual CS file would be like: # $pkg->SUPER::_update_state_objs_cmode_zapi( # %opts, # zapi_field_translations => { # add_percentage => [qw(field1 field2)], # hyphenate_value => [qw(field3 field4)], # } # ); # # Additional translations can be specified by doing something like: # additional_translations => { # size_to_bytes => { # method => '_zapi_output_translate_size', # fields_to_translate => ['size'] # }, # }, # # All that would need to be defined in the specific CS package is how to # translate a single value. See NACL::CS::VolumeSnapshot for example. # # Other custom translations can be handled with a mapping hash specified # through "hash_map_translations". Here's an example of how it is to be # specified: # hash_map_translations => { # fieldname => { # fields_to_translate => [qw(fieldname)], # hash_map => { # zapival1 => clival1, # zapival2 => clival2, # }, # }, # sub _update_state_objs_cmode_zapi { $Log->enter() if $may_enter; my ($pkg, @args) = @_; state $spec = { zapi_field_translations => {type => HASHREF, default => {}}, additional_translations => {type => HASHREF, default => {}}, hash_map_translations => {type => HASHREF, default => {}}, }; my %opts = validate_with( params => \@args, spec => $spec, allow_extra => 1, ); ## w.r.t burt897419, update the zapi tranlation hash using help_xml $pkg->_update_zapi_field_translation(\%opts); my $zapi_field_translations = delete $opts{zapi_field_translations}; my $additional_translations = delete $opts{additional_translations}; my $hash_map_translations = delete $opts{hash_map_translations}; if (keys %$zapi_field_translations) { my %known_translations = ( add_percentage => '_zapi_output_translate_percentage', hyphenate_value => '_zapi_output_translate_to_hyphens', on_off_boolean => '_zapi_output_translate_on_off', decimal_to_hex => '_zapi_output_translate_decimal_to_hex', ucfirst => '_zapi_output_translate_ucfirst', timestamp_to_string => '_zapi_output_translate_timestamp', uc => '_zapi_output_translate_uc', enable_disable => '_zapi_output_translate_enable_disable', timestamp_to_mmddyyyy => '_zapi_output_translate_to_mmddyyyy', timestamp_to_mmdd => '_zapi_output_translate_to_mmdd', sec_to_day_hrminsec => '_zapi_output_translate_day_hrminsec', sec_to_hrminsec => '_zapi_output_translate_hrminsec', decimal_to_octal => '_zapi_output_translate_decimal_to_octal', yes_no_boolean => '_zapi_output_translate_yes_no', integer_cron_time => '_zapi_output_translate_integer_cron_time', unix_perm_to_str => '_zapi_output_translate_unix_perm_to_str', language => '_zapi_output_translate_language', ); while (my ($key, $arrayref) = each %$zapi_field_translations) { my $translation_method = $known_translations{$key}; if (!defined $translation_method) { $Log->exit() if $may_exit; NATE::BaseException->throw('Internal NACL error: unknown ' . "key '$key' provided in 'zapi_field_translations"); } $pkg->$translation_method(%opts, fields_to_translate => $arrayref); } } state $add_translation_spec = { method => {type => SCALAR}, fields_to_translate => {type => ARRAYREF}, }; while (my ($key, $hashref) = each %$additional_translations) { try { validate_with( params => $hashref, spec => $add_translation_spec, ); } otherwise { my $exception = $_[0]; $exception->set_text('Internal NACL error: the field ' . "'additional_translations' in " . '_update_state_objs_cmode_zapi was provided incorrectly. ' . "Error:\n" . $exception->text() ); $Log->exit() if $may_exit; $exception->throw(); }; $pkg->_zapi_output_translate_val( %opts, translation_method => $hashref->{method}, fields_to_translate => $hashref->{fields_to_translate} ); } state $hash_map_spec = { fields_to_translate => {type => ARRAYREF}, hash_map => {type => HASHREF}, }; while (my ($key, $hashref) = each %$hash_map_translations) { try { validate_with( params => $hashref, spec => $hash_map_spec, ); } otherwise { my $exception = $_[0]; $exception->set_text('Internal NACL error: the field ' . "'hash_map_translations' in " . '_update_state_objs_cmode_zapi was provided incorrectly. ' . "Error:\n" . $exception->text() ); $Log->exit() if $may_exit; $exception->throw(); }; $pkg->_zapi_output_translate_val( %opts, translation_method => '_zapi_output_base_hash_map', fields_to_translate => $hashref->{fields_to_translate}, additional_args => $hashref->{hash_map}, ); } $Log->exit() if $may_exit; } sub _zapi_output_translate_percentage { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_percentage_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_percentage_translation_code { my ($pkg, $val) = @_; $val .= '%'; return $val; } sub _zapi_output_translate_decimal_to_hex { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_decimal_to_hex_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_decimal_to_hex_translation_code { my ($pkg, $val) = @_; if($val =~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/){ return sprintf("%x", $val); }else{ return $val; } } sub _zapi_output_translate_to_hyphens { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_hyphen_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_hyphen_translation_code { my ($pkg, $val) = @_; $val =~ s/_/-/g; return $val; } sub _zapi_output_translate_on_off { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_on_off_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_on_off_translation_code { my ($pkg, $val) = @_; if ($val eq 'true') { return 'on'; } elsif ($val eq 'false') { return 'off'; } else { return $val; } } sub _zapi_output_translate_enable_disable { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_enable_disable_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_enable_disable_translation_code { my ($pkg, $val) = @_; if ($val eq 'true') { return 'enabled'; } elsif ($val eq 'false') { return 'disabled'; } else { return $val; } } sub _zapi_output_translate_ucfirst { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_ucfirst_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_ucfirst_translation_code { my ($pkg, $val) = @_; return ucfirst $val; } sub _zapi_output_translate_uc { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_uc_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_uc_translation_code { my ($pkg, $val) = @_; return uc $val; } sub _zapi_output_translate_timestamp { $Log->enter() if $may_enter; my ($pkg, @args) = @_; state $spec = { state_objs => {type => ARRAYREF}, fields_to_translate => {type => ARRAYREF}, command_interface => _ontap_ci_validate_spec(), }; my %opts = validate_with( params => \@args, spec => $spec, allow_extra => 1, ); my $timezone; foreach my $cs (@{$opts{state_objs}}) { foreach my $field (@{$opts{fields_to_translate}}) { my $timestamp = $cs->$field(); if (defined $timestamp && $timestamp ne '-') { if (!defined $timezone) { try { $timezone = $opts{command_interface}->get_timezone( apiset_should => {interface => 'ZAPI'}); } catch NATE::Exceptions::Argument with { my $exception = $_[0]; $Log->warn('Attempt to translate timestamp fields ' . "to a CLI-like time-string failed " . 'because the timezone could not be discovered. ' . "Reason:\n" . $exception->text() ); }; # If $timezone is not filled in, then it means it could # not be discovered, so just return immediately. if (!defined $timezone) { $Log->exit() if $may_exit; return; } } my $hash = $pkg->convert_to_system_time( unixtime => $timestamp, timezone => $timezone, interface => 'ZAPI' ); # Time-string is like "Thu Mar 27 01:47:52 2014", i.e # "Day-of-week Month DD hh:mm:ss YYYY" my $day_of_week = $pkg->enumerate_week( wday => $hash->{day_of_week}, reverse => 1, ); my $month = $pkg->enumerate_month(month => $hash->{month}, reverse => 1); my $timestr = '"' . "$day_of_week $month $hash->{day} " . "$hash->{hour}:$hash->{minute}:" . "$hash->{second} $hash->{year}" . '"'; $cs->$field($timestr); } } } $Log->exit() if $may_exit; } sub _zapi_output_translate_to_mmddyyyy { $Log->enter() if $may_enter; my ($pkg_or_obj, %opts) = @_; $opts{has_year} = 1; $pkg_or_obj->_zapi_output_translate_date_time_wrapper(%opts); $Log->exit() if $may_exit; } ## end sub _zapi_output_translate_to_mmddyyyy sub _zapi_output_translate_to_mmdd { $Log->enter() if $may_enter; my ($pkg_or_obj, %opts) = @_; $pkg_or_obj->_zapi_output_translate_date_time_wrapper(%opts); $Log->exit() if $may_exit; } ## end sub _zapi_output_translate_to_mmdd sub _zapi_output_translate_date_time_wrapper { $Log->enter() if $may_enter; my ($pkg_or_obj, @args) = @_; state $spec = { state_objs => {type => ARRAYREF}, fields_to_translate => {type => ARRAYREF}, command_interface => _ontap_ci_validate_spec(), has_year => {type => SCALAR, default => 0} }; my %opts = validate_with( params => \@args, spec => $spec, allow_extra => 1, ); my ($timezone, $datestr); foreach my $cs (@{$opts{state_objs}}) { foreach my $field (@{$opts{fields_to_translate}}) { my $timestamp = $cs->$field(); if (defined $timestamp && $timestamp ne '-') { if (!defined $timezone) { try { $timezone = $opts{command_interface}->get_timezone( apiset_should => {interface => 'ZAPI'}); } catch NATE::Exceptions::Argument with { my $exception = $_[0]; $Log->warn('Attempt to translate timestamp fields ' . "to a CLI-like time-string failed " . 'because the timezone could not be discovered. ' . "Reason:\n" . $exception->text()); }; # If $timezone is not filled in, then it means it could # not be discovered, so just return immediately. if (!defined $timezone) { $Log->exit() if $may_exit; return; } } my $hash = $pkg_or_obj->convert_to_system_time( unixtime => $timestamp, timezone => $timezone, interface => 'ZAPI' ); if ($opts{year}) { $datestr = "$hash->{month}/$hash->{day}/$hash->{year} "; } else { $datestr = "$hash->{month}/$hash->{day} "; } my $timestr = '"' . $datestr . "$hash->{hour}:$hash->{minute}:$hash->{second}" . '"'; $cs->$field($timestr); } } } $Log->exit() if $may_exit; } ## end sub _zapi_output_translate_wrapper sub _zapi_output_translate_day_hrminsec { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_day_hr_min_sec_translation_code'); $Log->exit() if $may_exit; } ## end sub _zapi_output_translate_day_hrminsec sub _zapi_output_day_hr_min_sec_translation_code { my ($pkg_or_obj, $sec) = @_; $Log->enter() if $may_enter; my $str = $pkg_or_obj->_zapi_output_translate_time_wrapper(sec => $sec, has_day => 1); $Log->exit() if $may_exit; return $str; } ## end sub _zapi_output_day_hr_min_sec_translation_code sub _zapi_output_translate_hrminsec { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_hr_min_sec_translation_code'); $Log->exit() if $may_exit; } ## end sub _zapi_output_translate_hrminsec sub _zapi_output_hr_min_sec_translation_code { my ($pkg_or_obj, $sec) = @_; $Log->enter() if $may_enter; my $str = $pkg_or_obj->_zapi_output_translate_time_wrapper(sec => $sec); $Log->exit() if $may_exit; return $str; } ## end sub _zapi_output_hr_min_sec_translation_code sub _zapi_output_translate_time_wrapper { $Log->enter() if $may_enter; my ($pkg_or_obj, @args) = @_; state $spec = { has_day => {type => SCALAR, default => 0}, sec => {type => SCALAR } }; my %opts = validate_with( params => \@args, spec => $spec, ); my ($days, $hr, $min, $str); my $sec = $opts{sec}; $days = int($sec / (24 * 60 * 60)); $hr = ($sec / (60 * 60)) % 24; $min = ($sec / 60) % 60; $sec = $sec % 60; if ( !$opts{has_day} ) { if ( $days ) { $hr += $days * 24; } $str .= $hr . "h" if ($hr); $str .= $min . "m" if ($min); $str .= $sec . "s" if ($sec); } else { $str .= $days." days" if ($days); $str .= $hr if ($hr); $str .= ":" . $min if ($min); $str .= $sec if ($sec); } $Log->exit() if $may_exit; return $str; } sub _zapi_output_translate_decimal_to_octal { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_decimal_to_octal_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_decimal_to_octal_translation_code { my ($pkg, $val) = @_; return sprintf("%o", $val); } sub _zapi_output_translate_yes_no { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_yes_no_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_yes_no_translation_code { my ($pkg, $val) = @_; if ($val eq 'true') { return 'yes'; } elsif ($val eq 'false') { return 'no'; } else { return $val; } } sub _zapi_output_translate_integer_cron_time { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_integer_cron_time_translation_code'); $Log->exit() if $may_exit; } sub _zapi_integer_cron_time_translation_code { my ($pkg_or_obj, $val) = @_; my @new_val; if(ref $val){ @new_val = @$val; }else{ push @new_val, $val ; } map {s/^-1$/all/} @new_val; return \@new_val; } sub _zapi_output_translate_unix_perm_to_str { $Log->enter() if $may_enter; my ($pkg, @args) = @_; $pkg->_zapi_output_translate_val(@args, translation_method => '_zapi_output_unix_perm_translation_code'); $Log->exit() if $may_exit; } sub _zapi_output_unix_perm_translation_code { my ($pkg, $val) = @_; # This should be 4 digits. However, leading zeros are omitted, so if # length is less than 4, then keep prefixing 0's till length is 4. until (length ($val) == 4) { $val = 0 . $val; } # The first digit is the various IDs: set uid, set gid, and sticky bit # The next three are the file permissions # Example: 1755 # 1 = set ids # 755 = file permissions # Note: split on nothing = get each character in the string my ($setids, @perms) = split '', $val; # Set userid (4), set groupid (2), sticky bit (1) my %setids_hash = (0 => '---', 1 => '--t', 2 => '-s-', 3 => '-st', 4 => 's--', 5 => 's-t', 6 => 'ss-', 7 => 'sst'); # Permissions: r (4), w (2), x (1) my %perms_hash = (0 => '---', 1 => '--x', 2 => '-w-', 3 => '-wx', 4 => 'r--', 5 => 'r-x', 6 => 'rw-', 7 => 'rwx'); # Get the equivalent string notation from the pre-defined lists # Using example of 1755, each individual element would be: # '--t', 'rwx', 'r-x', 'r-x' my $str = $setids_hash{$setids}; foreach my $perm (@perms) { $str .= $perms_hash{$perm}; } return $str; } my %lang_map; sub _get_zapi_output_hash_map_language { # Already calculated, so return return \%lang_map if (keys %lang_map); # The ZAPI value for "language" does not match the CLI value. # The ZAPI value is all lower-case and has underscores instead of # hyphens. For example, the CLI value "zh_TW.UTF-8" would map to the # ZAPI value "zh_tw.utf_8". # Unfortunately, there's no easy way to convert from the ZAPI value to the # CLI value. We cannot upper-case all lower-case characters, because # many CLI values are also all lower-case (example: "ar"). Also, # we cannot convert all underscores to hyphens because some CLI # values have underscores (example: "en_US"). This is not pretty, # but here we maintain a full list of the known CLI options. The # equivalent ZAPI value is calculated and a map if computed. my @known_langs = ( qw(C da de en en_US es fi fr he it ja ja_JP.PCK ko no nl pt sv zh zh.GBK zh_TW zh_TW.BIG5 C.UTF-8 ar ar.UTF-8 cs cs.UTF-8 da.UTF-8 de.UTF-8 en.UTF-8 en_US.UTF-8 es.UTF-8 fi.UTF-8 fr.UTF-8 he.UTF-8 hr hr.UTF-8 hu hu.UTF-8 it.UTF-8 ja.UTF-8 ja_v1 ja_v1.UTF-8 ja_JP.PCK.UTF-8 ja_JP.932 ja_JP.932.UTF-8 ja_JP.PCK_v2 ja_JP.PCK_v2.UTF-8 ko.UTF-8 no.UTF-8 nl.UTF-8 pl pl.UTF-8 pt.UTF-8 ro ro.UTF-8 ru ru.UTF-8 sk sk.UTF-8 sl sl.UTF-8 sv.UTF-8 tr tr.UTF-8 zh.UTF-8 zh.GBK.UTF-8 zh_TW.UTF-8 zh_TW.BIG5.UTF-8) ); # The "undefined" language is denoted in both CLI and ZAPI as "-", # so map hyphen to itself. %lang_map = ('-' => '-'); foreach my $known_lang (@known_langs) { my $zapi_lang = lc $known_lang; $zapi_lang =~ s/-/_/g; $lang_map{$zapi_lang} = $known_lang; } return \%lang_map; } sub _zapi_output_translate_language { $Log->enter() if $may_enter; my ($pkg, %opts) = @_; my $lang_map = $pkg->_get_zapi_output_hash_map_language(); $pkg->_zapi_output_translate_val( %opts, translation_method => '_zapi_output_base_hash_map', additional_args => $lang_map, ); $Log->exit() if $may_exit; } sub _zapi_output_base_hash_map { my ($pkg_or_obj, $val, $option, %hash_map) = @_; if (exists $hash_map{$val}) { return $hash_map{$val}; } else { $Log->warn($pkg_or_obj->get_package_name() . ": The $option returned by the ZAPI was '$val' which is an " . 'unknown value. (Known ZAPI->CLI translations are: ' . NACL::ComponentUtils::_dump_one(\%hash_map) . ') This is either a product bug or this requires updating the ' . 'NACL library. Please inform dl-nacl-dev of this issue' ); return $val; } } sub _zapi_output_translate_val { $Log->enter() if $may_enter; my ($pkg, @args) = @_; state $spec = { state_objs => {type => ARRAYREF}, fields_to_translate => {type => ARRAYREF}, translation_method => {type => SCALAR}, additional_args => {type => HASHREF, default => {}}, }; my %opts = validate_with( params => \@args, spec => $spec, allow_extra => 1, ); my $translation_method = $opts{translation_method}; my $additional_args = $opts{additional_args}; foreach my $cs (@{$opts{state_objs}}) { foreach my $translate_option (@{$opts{fields_to_translate}}) { my $val = $cs->$translate_option(); if (ref $val eq 'ARRAY') { my $modified_array_val = (); foreach my $value (@$val) { if ($value ne '-') { $value = $pkg->$translation_method($value, $translate_option, %$additional_args); } push @$modified_array_val, $value; } $cs->$translate_option(@$modified_array_val); } elsif (defined ($val) && $val ne '-') { my $new_val = $pkg->$translation_method($val, $translate_option, %$additional_args); $cs->$translate_option($new_val); } } } $Log->exit() if $may_exit; } =head2 _fetch_cmode_zapi # NACL/CS/Foo.pm sub _fetch_cmode_zapi { my $pkg = shift; return $pkg->SUPER::_fetch_cmode_zapi (@_, api => "foo-get-iter", map => { component_state_field1 => "zapi-field1" component_state_field2 => ["internal-struct","zapi-field2"], component_state_field3 => ["internal-struct","zapi-field3"], }, [ copy => [ qw(component_state_field4) ], ] ); } Derived classes that need to implement a C-mode ZAPI backend for fetch can pass control to this base class method to do most of the work. This routine will only work to drive ZAPI that follow a particular newer C-mode style of inputs (desired-attributes, max-records, query, tag) and outputs (attributes-list, num-records, next-tag). These methods tend to be named "(something)-get-iter". For older style methods, often named "(something)-list-info", there's too much variation in inputs and outputs, so derived class methods that need to call those should avoid calling into here and instead just call on $apiset themselves. =over =item Options =over =item C<< command_interface=>$command_interface >> (Required, passed in by fetch frontend). See fetch. Not used here. =item C<< apiset=>$apiset >> (Required, passed in by fetch frontend). See fetch. =item C<< requested_fields=>$requested_fields >> (Required, passed in by fetch frontend). See fetch. These settings will be used as the "desired-attributes" when calling $apiset->$api(). =item C<< filter=>$filter >> (Required, passed in by fetch frontend). See fetch. These settings will be used as the "query" when calling $apiset->$api(). =item C<< api=>$apiset_method >> Which method on $apiset to call to fetch the data. =item C<< pass_through_opts >> These are the additional first level options in the same level as 'query' and 'desired-attributes' which usually many other get-iter ZAPIs don't take. =item C<< map=>$map >> (Optional, at least one of map or copy should be provided) A hashref mapping from the component state attribute names for this component (which are conventionally the same as the C-mode CLI column names, so we call these "cli" fields here) into the names used in the relevant type in the ZAPI call. This mapping is used to map "required_fields" and "filter" options into "desired-attributes" and "query" inputs, and then the reverse mapping is also used to map "attributes-list" data into component state names. In more complete detail, also relating to the APISet mapping of perl data structures to and from ZAPI XML requests, if the map contains a scalar mapping, such as from key $cli_1 to value $zapi_1, then: * If $cli_1 appears in "requested_fields", then the value of the desired-attributes option to $apiset->$api() will be set to the empty string. $desired_attributes->{$zapi_1}=""; * If $cli_1 appears as a key in "filter", then the value of the query option to $apiset->$api() will be set to the value of the key in the filter: $query->{$zapi_1}=$filter{$cli_1}; * If $zapi_1 appears in the response, then the $cli_1 field in the component state will be set to the value of that attribute of that element in the response: $component_state->$cli_1() = $parsed_output->[0]->{"attributes-list"}->[0]->{$response_typedef} ->[$element_number]->{$zapi_1}; If the map contains an array mapping, such as from key $cli_1 to arrayref ['a','b','c'], then the behavior is extended for "a", "b", and "c" to be a path to sub-fields and sub-typedefs within the response: * desired_attributes, when applicable, becomes: $desired_attributes->{a}{b}{c}=""; * query, when applicable, becomes: $query->{a}{b}{c}=$filter{$cli_1}; * mappings from the response data become: $component_state->$cli_1() = $parsed_output->[0]->{"attributes-list"}->[0]->{$response_typedef} ->[$element_number]->{a}->[0]->{b}->[0]->{c}; =item C<< copy => $copy >> (Optional, at least one of copy or map should be specified) This arrayref specifies that all the fields specified in it have the same name in the ZAPI call. For example, providing copy => [ qw(a) ] is equivalent to doing map => { a => ['a'] } =item C<< fix_* => $coderef >> (Optional) These callbacks are advanced/optional, and would be used to deal with mappings between CLI and ZAPI that are too complex for the "map" option to deal with. =over =item C<< fix_desired_attributes => $coderef >> Called by _fetch_cmode_zapi after desired-attributes is computed but before it's used, as follows: $coderef->(desired_attributes=>$desired_attributes, requested_fields=>$requested_fields); It should modify $desired_attributes in place, and it is also given the caller's $requested_fields setting to possibly copy data from. This callback isn't called if requested_fields was unset (in which case desired-attributes is always unset) =item C<< fix_query => $coderef >> As fix_desired_attributes, above, but modifying "query" based on "filter", rather than "desired-attributes" based on "requested_fields": $coderef->(query=>$query, filter=>$filter); This callback isn't called if filter is unset or empty (in which case query is always unset) =item C<< fix_response => $coderef >> Called by _fetch_cmode_zapi after it has taken the data from a ZAPI response and mapped it to a set of component state options, and is about to set those settings on a component state object that it will return. Called as follows: $coderef->(cli=>$cli, zapi=>$zapi); where the $zapi value is an element in the attributes-list in the response: $parsed_output->[0]->{"attributes-list"}->[0]->{$typedef}->[$element] and $cli is a hash of options to pass to ->new() to construct this component state. It should modify the $cli value in place, possibly using the data in $zapi as a reference. =item C<< pass_through_opts => \%hashref >> A hash-ref of options wihch should be passed through to the APISet call. =item C<< fix_api_opts => $coderef >> This option allows arbitrary modifications of the options sent to the API call. The coderef will be sent a hash-reference with "api_opts" as its only key, with its value being the API options. For example, here's how to use this option to ensure the "desired-attributes" is not sent to the API call: $pkg->SUPER::_fetch_cmode_zapi( %other_opts, fix_api_opts => sub { my %opts = @_; my $api_opts = $opts{api_opts}; delete $api_opts->{'desired-attributes'}; } ); =item C<< fix_records => $coderef >> This can be used to modify the records, where "records" refers to the records of parsed output. The coderef will be sent a hash with "records" as its only key. It is expected to return the new updated records (which should also be an array-of-hash-references). $pkg->SUPER::_fetch_cmode_zapi( %other_opts, fix_records => sub { my %opts = @_; my $records = $opts{records}; my @new_records; # ... Process $records here ... return \@new_records; } ); =item C<< do_not_warn_for_missing_attribute => $scalar >> Boolean defaults to 0, When set to 1, if the zapi api call is not returning the requested attribute it would not throw any warning =back =back =back =cut # Keyed by CS package name, each such entry contains entries for each # of the fields CMode ZAPI back-ends claim they can fill in but haven't # been defined as MethodMaker fields. my %cmode_zapi_warnings; # For each ZAPI, if the vserver attribute is not obtained in the output # when requested in the input, then log a warning and keep track of it. my %log_warn_once_for_vserver_attribute; sub _fetch_cmode_zapi { $Log->enter() if $may_enter; my $pkg = shift; my %opts = validate @_, { %{ $pkg->_fetch_backend_validate_spec() }, api => { type => SCALAR }, map => { type => HASHREF, default => {} }, copy => { type => ARRAYREF, default => [] }, # TODO: Very few ComponentState subclasses specify either # min_records OR max_records. This means min_records & # max_records will almost always be the default values, # and we get very little coverage of tag/next-tag behavior. # We should be ratcheting these downward... min_records => { type => SCALAR, default => param_global()->get('NACL_COMPONENT_ZAPI_MIN_RECORDS') // 1000 }, max_records => { type => SCALAR, default => param_global()->get('NACL_COMPONENT_ZAPI_MAX_RECORDS') // 100000 }, fix_desired_attributes => { type => CODEREF, optional => 1 }, fix_query => { type => CODEREF, optional => 1 }, fix_response => { type => CODEREF, optional => 1 }, pass_through_opts => { type => HASHREF, default => {} }, fix_api_opts => { type => CODEREF, optional => 1 }, fix_records => { type => CODEREF, optional => 1 }, do_not_warn_for_missing_attribute => { type => SCALAR, default => 0 }, inapplicable_for_help_xml => {type => ARRAYREF, default => []}, suppress_type_warning => {type => SCALAR, default => 0}, skip_auto_translation => {type => SCALAR, default => 0} }; if ( ( !keys %{ $opts{map} } ) && ( !@{ $opts{copy} } ) ) { NATE::Exceptions::Argument->throw( "Internal error: Either of 'map' " . "or 'copy' should have been provided as argument to " . "'_fetch_cmode_zapi()' but neither were" ); } my $apiset = $opts{apiset}; my $api = $opts{api}; my $command_interface = $opts{command_interface}; my $pass_through_opts = $opts{pass_through_opts}; my $requested_fields = $opts{requested_fields}; my $do_not_warn_for_missing_attribute = $opts{do_not_warn_for_missing_attribute}; my $skip_auto_translate = delete $opts{skip_auto_translation}; my %set_fields_opts = (suppress_type_warning => $opts{suppress_type_warning}); # If it came through here, then don't call _apply_filter ${ $opts{_apply_filter} } = 0; # increase timeout (Burt 433476) #$apiset->set_timeout( 'zapi-timeout' => 60 ); #Update mapping if ( $opts{show_cmd} && $command_interface->can_get_version_details() ) { $pkg->_update_cli_zapi_mapping(\%opts); } my @cli_to_zapi = %{ $opts{map} }; # copy => [ qw(a b) ] is equivalent to doing map => {a => ['a'], b => ['b']} # For each element in copy, we make an entry of this form foreach ( @{ $opts{copy} } ) { push @cli_to_zapi, ( $_ => [$_] ); } my @zapi_to_cli = reverse @cli_to_zapi; my @state_objs; my %api_opts; %api_opts = %{$pass_through_opts}; # Build 'desired-attributes' if values are passed in requested_fields # i.e. if ( $pkg->_want_some_fields($requested_fields) ) { my %source; # Since the APISet call will be expanding arrays according to # the typedef information, it cares about which kind of empty # value is passed in (empty string vs. empty array). Use # accessor methods on an empty component state object to # figure out which component state fields are arrays. # The product requires desired-attributes for arrays to have # the inner name as well in order to include the array in the # response (e.g. it wants , not just # ), so pass the contents for an array attribute # as [''] instead of just []. foreach my $req_field (@$requested_fields) { $source{$req_field} = ( ref $pkg->$req_field() eq 'ARRAY' ) ? [''] : ''; } my $desired_attributes = $pkg->_zapi_hash_copy( source => \%source, map => \@cli_to_zapi, target_skips => 1, ); # Fix up desired-attributes for array fields here if ( $opts{fix_desired_attributes} ) { $opts{fix_desired_attributes}->( requested_fields => $requested_fields, desired_attributes => $desired_attributes ); } $api_opts{'desired-attributes'} = $desired_attributes; } if ( $opts{filter} && %{ $opts{filter} } ) { my $query = $pkg->_zapi_hash_copy( source => $opts{filter}, map => \@cli_to_zapi, target_skips => 1, ); if ( $opts{fix_query} ) { $opts{fix_query}->( filter => $opts{filter}, query => $query ); } $api_opts{query} = $query; } my %cli_to_zapi_hash = @cli_to_zapi; my %fields_that_should_be_filled_in = $pkg->_calculate_fields_to_be_filled_in( %opts, cli_to_zapi_hash => \%cli_to_zapi_hash); my $extra_filter_fields = $pkg->_extra_filter_fields(); if ( !exists $cmode_zapi_warnings{$pkg} ) { my @invalid_list; foreach my $cli_field (keys %cli_to_zapi_hash) { if (!$pkg->can($cli_field) && !grep {$_ eq $cli_field} @$extra_filter_fields) { $cmode_zapi_warnings{$pkg}{$cli_field} = 1; push @invalid_list, $cli_field; } } if (@invalid_list) { $Log->warn( "For '$pkg', the CMode-ZAPI back-end states that " . "it can fill in the following fields:\n\n" . join( ', ', @invalid_list ) . "\n\neven though these fields are not defined in " . 'MethodMaker. This is either because these fields are ' . 'wrongly mapped or because these fields need to be added ' . 'to MethodMaker. Please raise a burt against nacl ' . '(type=nacl;subtype=nacl_core) or mail ' . 'dl-nacl-dev@netapp.com regarding the issue.' ); } } my $fix_vserver = 0; my $vserver_name; if ( $command_interface->isa('NACL::C::Vserver') && $pkg->can('vserver') ) { $fix_vserver = 1; $vserver_name = $command_interface->hostrec->hostname; } $opts{min_records} = int($opts{min_records}); $opts{max_records} = int($opts{max_records}); if ( $opts{min_records} > $opts{max_records} ) { $opts{min_records} = $opts{max_records}; } $api_opts{'max-records'} = $opts{min_records}; if ($opts{fix_api_opts}) { $opts{fix_api_opts}->(api_opts => \%api_opts); } while (1) { my $response = $apiset->$api(%api_opts); if($response->get_parsed_output()->[0]{'num-records'} eq '0' && defined($response->get_parsed_output()->[0]{'next-tag'}) ){ # next-tag was recognised with 0 num-records.Retry it again, burt#1054117 sleep(5); $response = $apiset->$api(%api_opts); } my $output = $response->get_parsed_output(); my $num_records = $output->[0]{'num-records'}; my $attributes_list = $output->[0]{'attributes-list'}; my @record_keys = keys( %{ $attributes_list->[0] } ); if ( @record_keys > 1 ) { NATE::BaseException->throw( "attributes-list in response had more than one key/typedef below it:\n" . ' Response:' . Dumper($output) ); } my $records; $records = $attributes_list->[0]{ $record_keys[0] } if ( defined $record_keys[0] ); last if ( $num_records eq '0' && !$records ); if ( $num_records != @{$records} ) { NATE::BaseException->throw( "num-records and number of records did not match:\n" . ' Response:' . Dumper($output) ); } my $C_pkg = $pkg->get_C_package_name(); my @primary_keys; # For CS like methods - Eg: NACL::C::Lun->stats which returns # NACL::CS::LunStats objects, we should not try to load # NACL::C::LunStats and fail eval "use $C_pkg"; # Get all the primary keys (default spec) by without passing ci # so that even the old pri key (if any) can also be obtained. # A value of hyphen should not be filled in for old primary keys also. unless ($@) { @primary_keys = $C_pkg->get_primary_keys(); } my %pri_keys_hash = map {$_ => 1} @primary_keys; if ($opts{fix_records}) { $records = $opts{fix_records}->(records => $records); } foreach my $record ( @{$records} ) { my $row = $pkg->_zapi_hash_copy( source => $record, map => \@zapi_to_cli, source_has_extra_arrays => 1, ); if ( $opts{fix_response} ) { $opts{fix_response}->( zapi => $record, cli => $row ); } my $obj = $pkg->new( command_interface => $command_interface ); $obj->_set_fields(row => $row, is_zapi => 1, need_to_quote => 1, %set_fields_opts); foreach my $key ( keys %fields_that_should_be_filled_in ) { next if ( exists $cmode_zapi_warnings{$pkg}{$key} ); my $isset = "${key}_isset"; if ( !$obj->$isset() ) { if ( $key eq 'vserver' ) { $obj->$key($vserver_name); if ( !$log_warn_once_for_vserver_attribute{$api} && !$do_not_warn_for_missing_attribute ) { $Log->warn( "The ZAPI '$api' has failed to return " . '\'vserver\' information in the output XML, even' . ' when the field was requested. A product burt ' . 'needs to be filed for further clarification.' ); $log_warn_once_for_vserver_attribute{$api} = 1; } } else { unless ($pri_keys_hash{$key}) { # If it is not a pri key, then fill it with hyphen $obj->$key('-'); } } } } push @state_objs, $obj; } # copy next-tag from output to next request last if ( !$output->[0]{'next-tag'} ); $api_opts{tag} = $output->[0]{'next-tag'}; if ( $api_opts{'max-records'} < $opts{max_records} ) { # max-records increases towards upper bound $opts{max_records} $api_opts{'max-records'} = List::Util::min($api_opts{'max-records'} * 2, $opts{max_records}); } } my %extra_opts = (); if($opts{show_cmd} && !$skip_auto_translate){ %extra_opts = ( show_cmd => $opts{show_cmd}, fields_that_should_be_filled_in => \%fields_that_should_be_filled_in, help_xml_obj => $opts{help_xml_obj}, zapi_field_translations => {}, ); } $pkg->_update_state_objs_cmode_zapi( state_objs => \@state_objs, command_interface => $opts{command_interface}, %extra_opts ); $Log->exit() if $may_exit; return @state_objs; } ## Private subroutine to handle the addition of new tranlation ## fields sub _update_zapi_field_translation { my ( $pkg_or_obj, $opts ) = @_; $Log->enter() if $may_enter; my $realm = NACL::Realm->realm(); my $command_interface = $opts->{command_interface}; my $xml_obj = delete $opts->{help_xml_obj}; ## There are many lib where translation logic is implemented after ## state objects are obtained my @not_allowed_translations = qw(decimal_to_hex timestamp_to_string timestamp_to_mmddyyyy timestamp_to_mmdd sec_to_day_hrminsec sec_to_hrminsec decimal_to_octal integer_cron_time unix_perm_to_str); if ( defined $xml_obj && $xml_obj->help_xml_accessible() ) { my $api_cmd = $opts->{show_cmd}; $api_cmd =~ s/\s+/_/g; $api_cmd =~ s/-/_/g; my $cdef = $command_interface->apiset()->get_command_definition( command => $api_cmd ); my $alias_map = $cdef->get_alias_map(); my $new_translation_val = $xml_obj->get_zapi_translation( command => $opts->{show_cmd}, alias_map => $alias_map ); while ( my ( $key, $value ) = each %{$new_translation_val} ) { ## fields that are declared in MethodMaker must only be ## valid for translation if ( !grep ( /^$key$/, @not_allowed_translations ) ) { if ( defined $opts->{zapi_field_translations}->{$key} ) { my $arr_ref = $opts->{zapi_field_translations}->{$key}; my $lc = List::Compare->new( $arr_ref, $value ); my @new_fields = $lc->get_complement(); my @valid_fields; foreach my $field (@new_fields) { if ( exists $opts->{fields_that_should_be_filled_in}->{$field} ) { push @valid_fields, $field; } } @{$arr_ref} = ( @{$arr_ref}, @valid_fields ); $opts->{zapi_field_translations}->{$key} = $arr_ref; } else { my @valid_fields; foreach my $field ( @{$value} ) { if ( exists $opts->{fields_that_should_be_filled_in}->{$field} ) { push @valid_fields, $field; } } $opts->{zapi_field_translations}->{$key} = \@valid_fields if (@valid_fields); } } } } $Log->exit() if $may_exit; } sub _update_cli_zapi_mapping { my ( $pkg_or_obj, $opts ) = @_; $Log->enter() if $may_enter; my $realm = NACL::Realm->realm(); my $command_interface = $opts->{command_interface}; my $xml_obj = $realm->get_help_xml_obj( command_interface => $command_interface ); $opts->{help_xml_obj} = $xml_obj ; if ( $xml_obj->help_xml_accessible() ) { my @known_fields = (@{$opts->{copy}}, keys %{$opts->{map}}, @{$opts->{inapplicable_for_help_xml}}); my @req_filter_list = (@{$opts->{requested_fields}}, keys %{$opts->{filter}}); my $lc = List::Compare->new(\@known_fields, \@req_filter_list); my @new_fields = $lc->get_complement(); my $api_cmd = $opts->{show_cmd}; $api_cmd =~ s/\s+/_/g; $api_cmd =~ s/-/_/g; my $alias_map; if ( @new_fields ) { if( $command_interface->apiset(check_apiset_already_loaded => 1) ) { my $cdef = $command_interface->apiset()->get_command_definition(command => $api_cmd); $alias_map = $cdef->get_alias_map(); } my $mapping_info = $xml_obj->get_zapi_field_mapping(command => $opts->{show_cmd}, new_fields => \@new_fields, mapping_as_ds => 1, alias_map => $alias_map ); if ( $mapping_info ) { %{ $opts->{map} } = ( %{ $mapping_info }, %{ $opts->{map} } ); } } } $Log->exit() if $may_exit; } =head2 _fetch_7mode_cli_frontend_method_builder __PACKAGE__->_fetch_7mode_cli_frontend_method_builder; Provide a simple way to install methods at the ComponentState-level that act as the frontend fetch call for 7 mode CLI commands For example, installing a method for AggregateEcacheFind fetch is shown as below __PACKAGE__->_fetch_7mode_cli_frontend_method_builder; =over =cut sub _fetch_7mode_cli_frontend_method_builder { $Log->enter() if $may_enter; my ($pkg) = @_; my $method = 'fetch'; no strict 'refs'; # Build frontend method *{"${pkg}::$method"} = sub { my ( $sub_pkg, @sub_args ) = @_; my @state_objs = $sub_pkg->SUPER::fetch( @sub_args, choices => [ { method => '_fetch_7mode_cli', interface => 'CLI', set => '7Mode|Nodescope', }, ], exception_text => 'No matching found', frontend => "${pkg}::$method", ); $Log->exit() if $may_exit; return wantarray ? @state_objs : $state_objs[0]; }; use strict 'refs'; } =head2 _fetch_7mode_cli_backend_method_builder __PACKAGE__->_fetch_7mode_cli_backend_method_builder( filter_keys_to_be_passed_to_apiset => ['aggregate'] ); Provide a simple way to install methods at the ComponentState-level that invoke the backend 7mode CLI fetch method For example, installing a method that runs the backend fetch 7mode CLI command for Ecache find command __PACKAGE__->_fetch_7mode_cli_backend_method_builder( apiset_method => 'mw_tec_find', filter_keys_to_be_passed_to_apiset => [qw/aggregate key/], process_parsed_output => 1, ); =over =item Options =over =item C<< filter_keys_to_be_passed_to_apiset => ['key1','key2'] >> (Optional) Array of filter keys that should be passed to the apiset call =item C<< process_parsed_output => 1 >> (Optional) This makes the backend method invoke _process_parsed_output method on the package. Using this option will need a method _process_parsed_output to be implements in the CS package that will act on parsed output from the apiset =cut sub _fetch_7mode_cli_backend_method_builder { $Log->enter() if $may_enter; my $pkg = shift; my %args = validate_with( params => \@_, spec => { apiset_method => { type => SCALAR }, element_not_found_regex => { type => SCALAR, default => 'does\snot\sexist|not\sfound' }, process_parsed_output => { type => BOOLEAN, optional => 1 }, filter_keys_to_be_passed_to_apiset => { type => ARRAYREF, optional => 1 }, }, ); my $method = '_fetch_7mode_cli'; my $apiset_method = $args{apiset_method}; my $element_not_found_regex = $args{element_not_found_regex}; no strict 'refs'; # Build backend method *{"${pkg}::$method"} = sub { $Log->enter() if $may_enter; my $sub_pkg = shift; my %opts = validate @_, $sub_pkg->_fetch_backend_validate_spec(); my %apiset_input = (); map { $apiset_input{$_} = $opts{filter}{$_} if(defined $opts{filter}{$_}) } @{ $args{filter_keys_to_be_passed_to_apiset} }; my $caught_exception = 0; my $output; try { $output = $opts{apiset}->$apiset_method(%apiset_input)->get_parsed_output(); } catch NACL::APISet::Exceptions::CommandFailedException with { my $ex = shift; if( $ex->text =~ /$element_not_found_regex/is ) { $caught_exception = 1; } else { $ex->throw; } } catch NACL::APISet::Exceptions::InvalidParamException with { my $ex = shift; if( $ex->text =~ /$element_not_found_regex/is ) { $caught_exception = 1; } else { $ex->throw; } }; return if($caught_exception); $pkg->_process_parsed_output($output) if($args{process_parsed_output}); my @state_objs; for my $each_row (@$output) { my $obj = $pkg->new(command_interface => $opts{command_interface}); $obj->_set_fields(row => $each_row, need_to_quote => 0); push @state_objs, $obj; } $Log->exit() if $may_exit; return wantarray ? @state_objs : $state_objs[0]; }; use strict 'refs'; } =head2 _fetch_7mode_cli_method_builder __PACKAGE__->_fetch_7mode_cli_method_builder(%args); Provide a simple way to install methods at the ComponentState-level that act as the frontend and backend fetch call for 7 mode CLI commands for %args check _fetch_7mode_cli_backend_method_builder For example, installing a method for AggregateEcacheFind fetch is shown as below __PACKAGE__->_fetch_7mode_cli_method_builder( apiset_method => 'mw_tec_find', filter_keys_to_be_passed_to_apiset => [qw/aggregate key/], process_parsed_output => 1, ); =over =cut sub _fetch_7mode_cli_method_builder { my $pkg = shift; $pkg->_fetch_7mode_cli_frontend_method_builder; $pkg->_fetch_7mode_cli_backend_method_builder(@_); } sub _check_for_invalid_requested_filter_fields { $Log->enter() if $may_enter; my ($pkg, @args) = @_; my %opts = validate_with(params => \@args, spec => { command_interface => { isa => 'NACL::C::CommandInterface', type => OBJECT, optional => 1 }, show_cmd => { type => SCALAR|UNDEF, optional => 1 }, }, allow_extra => 1); my $ci = delete $opts{command_interface}; my $show_cmd = delete $opts{show_cmd}; my ( @unknown_input_fields, $datatype_hashref ); try { $pkg->SUPER::_check_for_invalid_requested_filter_fields(%opts); } catch NACL::Exceptions::InvalidFilterField with { my $e = shift; if ( $ci->is_cmode() && $show_cmd ) { push(@unknown_input_fields, $e->unknown_requested_fields()); push(@unknown_input_fields, $e->unknown_filters()); my $api_cmd = $show_cmd; $api_cmd =~ s/\s+/_/g; $api_cmd =~ s/-/_/g; my $help_obj = NACL::Realm->realm()->get_help_xml_obj(command_interface => $ci); if ($help_obj->help_xml_accessible()) { my $alias_map; if ( $ci->apiset(check_apiset_already_loaded => 1) ) { my $cdef = $ci->apiset()->get_command_definition(command => $api_cmd); $alias_map = $cdef->get_alias_map(); } $datatype_hashref = $help_obj->get_field_datatype(command => $show_cmd, fields => \@unknown_input_fields, alias_map => $alias_map); } else { $datatype_hashref = $help_obj->get_field_datatype_ui(command_interface => $ci, fields => \@unknown_input_fields, api => $api_cmd); } my @new_fields = keys %{ $datatype_hashref }; foreach my $field ( @new_fields ) { $pkg->_install_methodmaker_field(field => $field, type => $datatype_hashref->{$field}); } my %temp_hash; map { $temp_hash{$_}=1 } @unknown_input_fields; map { delete $temp_hash{$_} } @new_fields; if ( keys %temp_hash ) { my $err_str = 'Unknown field(s) '. join(",", keys %temp_hash ).' This could be ' . "because:\n" . "\t1. The field is invalid\n" . "\t2. The field is valid but has " . "not been implemented on the CS object '$pkg'. If " . 'this is the case, please raise a burt against ' . 'nacl (type=nacl;subtype=nacl_core) or mail ' . 'dl-nacl-dev@netapp.com regarding the issue'; $Log->exit() if $may_exit; NACL::Exceptions::InvalidFilterField->throw($err_str); } } else { $Log->exit() if $may_exit; $e->throw(); } }; } 1;