1 package FS::part_export::saisei;
4 use vars qw( @ISA %info );
5 use base qw( FS::part_export );
6 use Date::Format 'time2str';
18 FS::part_export::saisei
22 Saisei integration for Freeside
26 This export offers basic svc_broadband provisioning for Saisei.
28 This is a customer integration with Saisei. This will set up a rate plan and tie
29 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
30 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
32 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
33 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
35 To use this export, follow the below instructions:
37 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
38 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
39 Attach this Saisei export to this service.
41 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
42 Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
43 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
44 Each sector will be attached to its tower access point using the Saisei uplink field.
46 Create a package for the above created service, and order this package for a customer.
48 Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
49 This provisioned service will then be exported as a host to Saisei.
51 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
53 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link Export provisioned services attached to this export.
54 Clicking on this link will export all services attached to this export not currently exported to Saisei.
56 This module also provides generic methods for working through the L</Saisei API>.
60 tie my %scripts, 'Tie::IxHash',
61 'export_provisioned_services' => { component => '/elements/popup_link.html',
62 label => 'Export provisioned services',
63 description => 'will export provisioned services of part service with Saisei export attached.',
64 html_label => '<b>Export provisioned services attached to this export.</b>',
68 tie my %options, 'Tie::IxHash',
69 'port' => { label => 'Port',
71 'username' => { label => 'Saisei API User Name',
73 'password' => { label => 'Saisei API Password',
75 'debug' => { type => 'checkbox',
76 label => 'Enable debug warnings' },
80 'svc' => 'svc_broadband',
81 'desc' => 'Export broadband service/account to Saisei',
82 'options' => \%options,
83 'scripts' => \%scripts,
85 This is a customer integration with Saisei. This will set up a rate plan and tie
86 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
87 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
89 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
90 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
92 To use this export, follow the below instructions:
96 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
97 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
98 Attach this Saisei export to this service.
102 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
103 Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
104 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
105 Each sector will be attached to its tower access point using the Saisei uplink field.
109 Create a package for the above created service, and order this package for a customer.
113 Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
114 This provisioned service will then be exported as a host to Saisei.
116 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <i>none</i>.
120 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link <b>Export provisioned services attached to this export</b>.
121 Clicking on this link will export all services attached to this export not currently exported to Saisei.
130 my ($self, $svc_broadband) = @_;
132 my $rateplan_name = $self->get_rateplan_name($svc_broadband);
134 # check for existing rate plan
135 my $existing_rateplan;
136 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
138 # if no existing rate plan create one and modify it.
139 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
140 $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
141 return $self->api_error if $self->{'__saisei_error'};
143 # set rateplan to existing one or newly created one.
144 my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
146 my $username = $svc_broadband->{Hash}->{svcnum};
147 my $description = $svc_broadband->{Hash}->{description};
150 $self->{'__saisei_error'} = 'no username - can not export';
151 return $self->api_error;
154 # check for existing user.
156 $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
158 # if no existing user create one.
159 $self->api_create_user($username, $description) unless $existing_user;
160 return $self->api_error if $self->{'__saisei_error'};
162 # set user to existing one or newly created one.
163 my $user = $existing_user ? $existing_user : $self->api_get_user($username);
166 my $tower_sector = FS::Record::qsearchs({
167 'table' => 'tower_sector',
168 'select' => 'tower.towername,
169 tower.up_rate_limit as tower_upratelimit,
170 tower.down_rate_limit as tower_downratelimit,
171 tower_sector.sectorname,
172 tower_sector.up_rate_limit as sector_upratelimit,
173 tower_sector.down_rate_limit as sector_downratelimit ',
174 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
176 'sectornum' => $svc_broadband->{Hash}->{sectornum},
180 my $tower_name = $tower_sector->{Hash}->{towername};
181 $tower_name =~ s/\s/_/g;
184 'tower_name' => $tower_name,
185 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
186 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
189 my $tower_ap = process_tower($self, $tower_opt);
190 return $self->api_error if $self->{'__saisei_error'};
192 my $sector_name = $tower_sector->{Hash}->{sectorname};
193 $sector_name =~ s/\s/_/g;
196 'tower_name' => $tower_name,
197 'sector_name' => $sector_name,
198 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
199 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
201 my $accesspoint = process_sector($self, $sector_opt);
202 return $self->api_error if $self->{'__saisei_error'};
204 ## tie host to user add sector name as access point.
205 $self->api_add_host_to_user(
206 $user->{collection}->[0]->{name},
207 $rateplan->{collection}->[0]->{name},
208 $svc_broadband->{Hash}->{ip_addr},
209 $accesspoint->{collection}->[0]->{name},
210 ) unless $self->{'__saisei_error'};
213 return $self->api_error;
217 sub _export_replace {
218 my ($self, $svc_broadband) = @_;
219 $self->_export_insert($svc_broadband);
224 my ($self, $svc_broadband) = @_;
226 my $rateplan_name = $self->get_rateplan_name($svc_broadband);
228 my $username = $svc_broadband->{Hash}->{svcnum};
230 ## untie host to user
231 $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
236 sub _export_suspend {
237 my ($self, $svc_broadband) = @_;
241 sub _export_unsuspend {
242 my ($self, $svc_broadband) = @_;
247 my ($self, $svc_part) = @_;
249 if ( $FS::svc_Common::noexport_hack ) {
250 carp 'export_partsvc() suppressed by noexport_hack'
251 if $self->option('debug');
256 if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
257 for my $type (qw( down up )) {
258 my $speed_type = "broadband_".$type."stream";
259 foreach my $pkg_svc (FS::Record::qsearch({
260 'table' => 'pkg_svc',
261 'select' => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
262 'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
263 'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
264 })) { $fcc_477_speeds->{
265 $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
269 $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
270 $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
273 foreach my $key (keys %$fcc_477_speeds) {
275 $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
276 $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
277 $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
278 $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
280 my $temp_svc = $svc_part->{Hash};
281 my $svc_broadband = {};
282 map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
284 my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
286 # check for existing rate plan
287 my $existing_rateplan;
288 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
290 # Modify the existing rate plan with new service data.
291 $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
293 # if no existing rate plan create one and modify it.
294 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
295 $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
299 return $self->api_error;
303 sub export_tower_sector {
304 my ($self, $tower) = @_;
306 if ( $FS::svc_Common::noexport_hack ) {
307 carp 'export_tower_sector() suppressed by noexport_hack'
308 if $self->option('debug');
312 #modify tower or create it.
313 my $tower_name = $tower->{Hash}->{towername};
314 $tower_name =~ s/\s/_/g;
316 'tower_name' => $tower_name,
317 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
318 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
319 'modify_existing' => '1', # modify an existing access point with this info
322 my $tower_access_point = process_tower($self, $tower_opt);
324 #get list of all access points
326 'table' => 'tower_sector',
328 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
331 #for each one modify or create it.
332 foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
333 my $sector_name = $tower_sector->{Hash}->{sectorname};
334 $sector_name =~ s/\s/_/g;
336 'tower_name' => $tower_name,
337 'sector_name' => $sector_name,
338 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
339 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
340 'modify_existing' => '1', # modify an existing access point with this info
342 my $sector_access_point = process_sector($self, $sector_opt);
345 return $self->api_error;
348 ## creates the rateplan name
349 sub get_rateplan_name {
350 my ($self, $svc_broadband, $svc_name) = @_;
352 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
353 my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
355 my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
356 $rateplan_name =~ s/\s/_/g;
358 return $rateplan_name;
363 These methods allow access to the Saisei API using the credentials
364 set in the export options.
370 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
371 Places an api call to the specified path and method with the specified params.
372 Returns the decoded json object returned by the api call.
373 Returns empty on failure; retrieve error messages using L</api_error>.
378 my ($self,$method,$path,$params) = @_;
380 $self->{'__saisei_error'} = '';
381 my $auth_info = $self->option('username') . ':' . $self->option('password');
384 warn "Calling $method on http://"
385 .$self->{Hash}->{machine}.':'.$self->option('port')
386 ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
388 my $data = encode_json($params) if keys %{ $params };
390 my $client = REST::Client->new();
391 $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
392 $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
393 $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
395 warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
399 if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
400 eval { $result = decode_json($client->responseContent()) };
402 $self->{'__saisei_error'} = "Error decoding json: $@";
407 $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
408 unless ($method eq "GET");
409 warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
419 Returns the error string set by L</Saisei API> methods,
420 or a blank string if most recent call produced no errors.
426 return $self->{'__saisei_error'} || '';
429 =head2 api_get_policies
431 Gets a list of global policies.
435 sub api_get_policies {
438 my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
439 return if $self->api_error;
440 $self->{'__saisei_error'} = "Did not receive any global policies"
441 unless $get_policies;
443 return $get_policies->{collection};
446 =head2 api_get_rateplan
448 Gets rateplan info for specific rateplan.
452 sub api_get_rateplan {
454 my $rateplan = shift;
456 my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
457 return if $self->api_error;
459 return $get_rateplan;
464 Gets user info for specific user.
472 my $get_user = $self->api_call("GET", "/users/$user");
473 return if $self->api_error;
478 =head2 api_get_accesspoint
480 Gets user info for specific access point.
484 sub api_get_accesspoint {
486 my $accesspoint = shift;
488 my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
489 return if $self->api_error;
491 return $get_accesspoint;
496 Gets user info for specific host.
504 my $get_host = $self->api_call("GET", "/hosts/$ip");
506 return if $self->api_error;
511 =head2 api_create_rateplan
517 sub api_create_rateplan {
518 my ($self, $svc, $rateplan) = @_;
520 $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
521 $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
523 my $new_rateplan = $self->api_call(
525 "/rate_plans/$rateplan",
527 'downstream_rate' => $svc->{Hash}->{speed_down},
528 'upstream_rate' => $svc->{Hash}->{speed_up},
530 ) unless $self->{'__saisei_error'};
532 $self->{'__saisei_error'} = "Rate Plan not created"
533 unless ($new_rateplan || $self->{'__saisei_error'});
535 return $new_rateplan;
539 =head2 api_modify_rateplan
541 Modify a new rateplan.
545 sub api_modify_rateplan {
546 my ($self,$svc,$rateplan_name) = @_;
549 my $policies = $self->api_get_policies();
551 foreach my $policy (@$policies) {
552 my $policyname = $policy->{name};
553 my $rate_multiplier = '';
554 if ($policy->{background}) { $rate_multiplier = ".01"; }
555 my $modified_rateplan = $self->api_call(
557 "/rate_plans/$rateplan_name/partitions/$policyname",
559 'restricted' => $policy->{assured}, # policy_assured_flag
560 'rate_multiplier' => $rate_multiplier, # policy_background 0.1
561 'rate' => $policy->{percent_rate}, # policy_percent_rate
565 $self->{'__saisei_error'} = "Rate Plan not modified after create"
566 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
574 =head2 api_modify_existing_rateplan
576 Modify a existing rateplan.
580 sub api_modify_existing_rateplan {
581 my ($self,$svc,$rateplan_name) = @_;
583 my $modified_rateplan = $self->api_call(
585 "/rate_plans/$rateplan_name",
587 'downstream_rate' => $svc->{Hash}->{speed_down},
588 'upstream_rate' => $svc->{Hash}->{speed_up},
592 $self->{'__saisei_error'} = "Rate Plan not modified"
593 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
599 =head2 api_create_user
605 sub api_create_user {
606 my ($self,$user, $description) = @_;
608 my $new_user = $self->api_call(
612 'description' => $description,
616 $self->{'__saisei_error'} = "User not created"
617 unless ($new_user || $self->{'__saisei_error'}); # should never happen
623 =head2 api_create_accesspoint
625 Creates a access point.
629 sub api_create_accesspoint {
630 my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
632 # this has not been tested, but should work, if needed.
633 my $new_accesspoint = $self->api_call(
635 "/access_points/$accesspoint",
637 'downstream_rate_limit' => $downratelimit,
638 'upstream_rate_limit' => $upratelimit,
642 $self->{'__saisei_error'} = "Access point not created"
643 unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
648 =head2 api_modify_accesspoint
650 Modify a new access point.
654 sub api_modify_accesspoint {
655 my ($self, $accesspoint, $uplink) = @_;
657 my $modified_accesspoint = $self->api_call(
659 "/access_points/$accesspoint",
661 'uplink' => $uplink, # name of attached access point
665 $self->{'__saisei_error'} = "Rate Plan not modified"
666 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
672 =head2 api_modify_existing_accesspoint
674 Modify a existing accesspoint.
678 sub api_modify_existing_accesspoint {
679 my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
681 my $modified_accesspoint = $self->api_call(
683 "/access_points/$accesspoint",
685 'downstream_rate_limit' => $downratelimit,
686 'upstream_rate_limit' => $upratelimit,
687 # 'uplink' => $uplink, # name of attached access point
691 $self->{'__saisei_error'} = "Access point not modified"
692 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
698 =head2 api_add_host_to_user
700 ties host to user, rateplan and default access point.
704 sub api_add_host_to_user {
705 my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
707 my $new_host = $self->api_call(
712 'rate_plan' => $rateplan,
713 'access_point' => $accesspoint,
717 $self->{'__saisei_error'} = "Host not created"
718 unless ($new_host || $self->{'__saisei_error'}); # should never happen
724 =head2 api_delete_host_to_user
726 unties host from user and rateplan.
727 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
731 sub api_delete_host_to_user {
732 my ($self,$user, $rateplan, $ip) = @_;
734 my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
735 return if $self->api_error;
736 $self->{'__saisei_error'} = "Did not receive a default rate plan"
737 unless $default_rate_plan;
739 my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
741 my $delete_host = $self->api_call(
746 'access_point' => '<none>',
747 'rate_plan' => $default_rateplan_name,
751 $self->{'__saisei_error'} = "Host not created"
752 unless ($delete_host || $self->{'__saisei_error'}); # should never happen
759 my ($self, $opt) = @_;
761 my $existing_tower_ap;
762 my $tower_name = $opt->{tower_name};
764 #check if tower has been set up as an access point.
765 $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
767 # modify the existing accesspoint if changing tower .
768 $self->api_modify_existing_accesspoint (
770 '', # tower does not have a uplink on sectors.
771 $opt->{tower_uprate_limit},
772 $opt->{tower_downrate_limit},
773 ) if $existing_tower_ap && $opt->{modify_existing};
775 #if tower does not exist as an access point create it.
776 $self->api_create_accesspoint(
778 $opt->{tower_uprate_limit},
779 $opt->{tower_downrate_limit}
780 ) unless $existing_tower_ap;
782 my $accesspoint = $self->api_get_accesspoint($tower_name);
788 my ($self, $opt) = @_;
790 my $existing_sector_ap;
791 my $sector_name = $opt->{sector_name};
793 #check if sector has been set up as an access point.
794 $existing_sector_ap = $self->api_get_accesspoint($sector_name);
796 # modify the existing accesspoint if changing sector .
797 $self->api_modify_existing_accesspoint (
800 $opt->{sector_uprate_limit},
801 $opt->{sector_downrate_limit},
802 ) if $existing_sector_ap && $opt->{modify_existing};
804 #if sector does not exist as an access point create it.
805 $self->api_create_accesspoint(
807 $opt->{sector_uprate_limit},
808 $opt->{sector_downrate_limit},
809 ) unless $existing_sector_ap;
811 # Attach newly created sector to it's tower.
812 $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
814 # set access point to existing one or newly created one.
815 my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
820 sub export_provisioned_services {
824 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
825 or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
828 my @svcparts = FS::Record::qsearch({
829 'table' => 'export_svc',
830 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
831 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
833 my $part_count = scalar @svcparts;
835 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
837 my @svcs = FS::Record::qsearch({
838 'table' => 'cust_svc',
839 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
840 'extra_sql' => " WHERE svcpart in ('".$parts."')",
843 my $svc_count = scalar @svcs;
846 for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
849 foreach my $svc (@svcs) {
850 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
851 ## check if service exists as host if not export it.
852 _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});