RT# 83204 - added link to service or tower causing error.
[freeside.git] / FS / FS / part_export / saisei.pm
index 2fc1127..8661651 100644 (file)
@@ -9,6 +9,7 @@ use MIME::Base64;
 use REST::Client;
 use Data::Dumper;
 use FS::Conf;
+use Carp qw(carp);
 
 =pod
 
@@ -61,6 +62,8 @@ tie my %scripts, 'Tie::IxHash',
                                       label     => 'Export provisioned services',
                                       description => 'will export provisioned services of part service with Saisei export attached.',
                                       html_label => '<b>Export provisioned services attached to this export.</b>',
+                                      error_url  => '/edit/part_export.cgi?',
+                                      success_message => 'Saisei export of provisioned services successful',
                                     },
 ;
 
@@ -93,7 +96,7 @@ To use this export, follow the below instructions:
 <OL>
 <LI>
 Create a new service definition and set the table to svc_broadband.  The service name will become the Saisei rate plan name.
-Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
+Set the upload speed, download speed, and tower to be required for the service. This is required to be able to export the service to Saisei.
 Attach this Saisei export to this service.
 </LI>
 <P>
@@ -109,7 +112,7 @@ Create a package for the above created service, and order this package for a cus
 </LI>
 <P>
 <LI>
-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.
+Provision the service, making sure to enter the IP address associated with this service, the upload and download speed are correct, and select the tower and sector for it's access point.
 This provisioned service will then be exported as a host to Saisei.
 <P>
 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>.
@@ -121,16 +124,14 @@ Clicking on this link will export all services attached to this export not curre
 </LI>
 </OL>
 <P>
-
+<A HREF="http://www.freeside.biz/mediawiki/index.php/Saisei_provisioning_export" target="_new">Documentation</a>
 END
 );
 
 sub _export_insert {
   my ($self, $svc_broadband) = @_;
 
-  my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
-  my $rateplan_name = $service_part->{Hash}->{svc};
-  $rateplan_name =~ s/\s/_/g;
+  my $rateplan_name = $self->get_rateplan_name($svc_broadband);
 
   # check for existing rate plan
   my $existing_rateplan;
@@ -170,6 +171,7 @@ sub _export_insert {
                       tower.up_rate_limit as tower_upratelimit,
                       tower.down_rate_limit as tower_downratelimit,
                       tower_sector.sectorname,
+                      tower_sector.towernum,
                       tower_sector.up_rate_limit as sector_upratelimit,
                       tower_sector.down_rate_limit as sector_downratelimit ',
       'addl_from' => 'LEFT JOIN tower USING ( towernum )',
@@ -183,6 +185,7 @@ sub _export_insert {
 
     my $tower_opt = {
       'tower_name'           => $tower_name,
+      'tower_num'            => $tower_sector->{Hash}->{towernum},
       'tower_uprate_limit'   => $tower_sector->{Hash}->{tower_upratelimit},
       'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
     };
@@ -195,19 +198,37 @@ sub _export_insert {
 
     my $sector_opt = {
       'tower_name'            => $tower_name,
+      'tower_num'             => $tower_sector->{Hash}->{towernum},
       'sector_name'           => $sector_name,
       'sector_uprate_limit'   => $tower_sector->{Hash}->{sector_upratelimit},
       'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
+      'rateplan'              => $rateplan_name,
     };
     my $accesspoint = process_sector($self, $sector_opt);
     return $self->api_error if $self->{'__saisei_error'};
 
+## get custnum and pkgpart from cust_pkg for virtual access point
+    my $cust_pkg = FS::Record::qsearchs({
+      'table'     => 'cust_pkg',
+      'hashref'   => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
+    });
+    my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
+
+    my $virtual_ap_opt = {
+      'virtual_name'           => $virtual_ap_name,
+      'sector_name'            => $sector_name,
+      'virtual_uprate_limit'   => $svc_broadband->{Hash}->{speed_up},
+      'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
+    };
+    my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
+    return $self->api_error if $self->{'__saisei_error'};
+
     ## tie host to user add sector name as access point.
     $self->api_add_host_to_user(
       $user->{collection}->[0]->{name},
       $rateplan->{collection}->[0]->{name},
       $svc_broadband->{Hash}->{ip_addr},
-      $accesspoint->{collection}->[0]->{name},
+      $virtual_ap->{collection}->[0]->{name},
     ) unless $self->{'__saisei_error'};
   }
 
@@ -217,15 +238,15 @@ sub _export_insert {
 
 sub _export_replace {
   my ($self, $svc_broadband) = @_;
-  return '';
+  my $error = $self->_export_insert($svc_broadband);
+  return $error;
 }
 
 sub _export_delete {
   my ($self, $svc_broadband) = @_;
 
-  my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
-  my $rateplan_name = $service_part->{Hash}->{svc};
-  $rateplan_name =~ s/\s/_/g;
+  my $rateplan_name = $self->get_rateplan_name($svc_broadband);
+
   my $username = $svc_broadband->{Hash}->{svcnum};
 
   ## untie host to user
@@ -247,25 +268,55 @@ sub _export_unsuspend {
 sub export_partsvc {
   my ($self, $svc_part) = @_;
 
-  my $rateplan_name = $svc_part->{Hash}->{svc};
-  $rateplan_name =~ s/\s/_/g;
-  my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down};
-  my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up};
+  if ( $FS::svc_Common::noexport_hack ) {
+    carp 'export_partsvc() suppressed by noexport_hack'
+      if $self->option('debug');
+    return;
+  }
 
-  my $temp_svc = $svc_part->{Hash};
-  my $svc_broadband = {};
-  map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
+  my $fcc_477_speeds;
+  if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
+    for my $type (qw( down up )) {
+      my $speed_type = "broadband_".$type."stream";
+      foreach my $pkg_svc (FS::Record::qsearch({
+        'table'     => 'pkg_svc',
+        'select'    => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
+        'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
+        'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
+      })) { $fcc_477_speeds->{
+        $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
+    }
+  }
+  else {
+    $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
+    $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
+  }
 
-  # check for existing rate plan
-  my $existing_rateplan;
-  $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
+  foreach my $key (keys %$fcc_477_speeds) {
 
-  # Modify the existing rate plan with new service data.
-  $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
+    $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
+    $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
+    $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
+    $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
 
-  # if no existing rate plan create one and modify it.
-  $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
-  $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+    my $temp_svc = $svc_part->{Hash};
+    my $svc_broadband = {};
+    map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
+
+    my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
+
+    # check for existing rate plan
+    my $existing_rateplan;
+    $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
+
+    # Modify the existing rate plan with new service data.
+    $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
+
+    # if no existing rate plan create one and modify it.
+    $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
+    $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+
+  }
 
   return $self->api_error;
 
@@ -274,17 +325,25 @@ sub export_partsvc {
 sub export_tower_sector {
   my ($self, $tower) = @_;
 
+  if ( $FS::svc_Common::noexport_hack ) {
+    carp 'export_tower_sector() suppressed by noexport_hack'
+      if $self->option('debug');
+    return;
+  }
+
   #modify tower or create it.
   my $tower_name = $tower->{Hash}->{towername};
   $tower_name =~ s/\s/_/g;
   my $tower_opt = {
     'tower_name'           => $tower_name,
+    'tower_num'            => $tower->{Hash}->{towernum},
     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
     'modify_existing'      => '1', # modify an existing access point with this info
   };
 
   my $tower_access_point = process_tower($self, $tower_opt);
+    return $tower_access_point if $tower_access_point->{error};
 
   #get list of all access points
   my $hash_opt = {
@@ -299,15 +358,30 @@ sub export_tower_sector {
     $sector_name =~ s/\s/_/g;
     my $sector_opt = {
       'tower_name'            => $tower_name,
+      'tower_num'             => $tower_sector->{Hash}->{towernum},
       'sector_name'           => $sector_name,
       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
       'modify_existing'       => '1', # modify an existing access point with this info
     };
-    my $sector_access_point = process_sector($self, $sector_opt);
+    my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
+      return $sector_access_point if $sector_access_point->{error};
   }
 
-  return $self->api_error;
+  return { error => $self->api_error, };
+}
+
+## creates the rateplan name
+sub get_rateplan_name {
+  my ($self, $svc_broadband, $svc_name) = @_;
+
+  my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
+  my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
+
+  my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
+  $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
+
+  return $rateplan_name;
 }
 
 =head1 Saisei API
@@ -335,30 +409,48 @@ sub api_call {
 
   warn "Calling $method on http://"
     .$self->{Hash}->{machine}.':'.$self->option('port')
-    ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
+    ."/rest/top/configurations/running/$path\n" if $self->option('debug');
 
   my $data = encode_json($params) if keys %{ $params };
 
   my $client = REST::Client->new();
   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
-  $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
+  $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
 
-  warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
+  warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
 
   my $result;
 
   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
     eval { $result = decode_json($client->responseContent()) };
     unless ($result) {
-      $self->{'__saisei_error'} = "Error decoding json: $@";
+      $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei.  Bad JSON data logged in error log if debug option was set.";
+      warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
+      return;
+    }
+  }
+  elsif ($client->responseCode() eq '404') {
+    eval { $result = decode_json($client->responseContent()) };
+    unless ($result) {
+      $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei.  Bad JSON data logged in error log if debug option was set.";
+      warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
       return;
     }
+    ## check if message is for empty hash.
+    my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
+    $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
+    warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
+    return;
+  }
+  elsif ($client->responseCode() eq '500') {
+    $self->{'__saisei_error'} = "Could not connect to the Saisei export host machine (".$self->{Hash}->{machine}.':'.$self->option('port').") during $method , we received the responce code: " . $client->responseCode();
+    warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
+    return;
   }
   else {
-    $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
-    unless ($method eq "GET");
-    warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
+    $self->{'__saisei_error'} = "Received Bad response from server during $method , we received responce code: " . $client->responseCode();
+    warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
     return; 
   }
 
@@ -389,7 +481,7 @@ sub api_get_policies {
 
   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
   return if $self->api_error;
-  $self->{'__saisei_error'} = "Did not receive any global policies"
+  $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
     unless $get_policies;
 
   return $get_policies->{collection};
@@ -455,7 +547,7 @@ sub api_get_host {
 
   my $get_host = $self->api_call("GET", "/hosts/$ip");
 
-  return if $self->api_error;
+  return { message => $self->api_error, } if $self->api_error;
 
   return $get_host;
 }
@@ -469,8 +561,8 @@ Creates a rateplan.
 sub api_create_rateplan {
   my ($self, $svc, $rateplan) = @_;
 
-  $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
-  $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
+  $self->{'__saisei_error'} = "There is no download speed set for the service !--service,".$svc->svcnum.",".$rateplan."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
+  $self->{'__saisei_error'} = "There is no upload speed set for the service !--service,".$svc->svcnum.",".$rateplan."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
 
   my $new_rateplan = $self->api_call(
       "PUT", 
@@ -481,7 +573,7 @@ sub api_create_rateplan {
       },
   ) unless $self->{'__saisei_error'};
 
-  $self->{'__saisei_error'} = "Rate Plan not created"
+  $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
     unless ($new_rateplan || $self->{'__saisei_error'});
 
   return $new_rateplan;
@@ -514,7 +606,7 @@ sub api_modify_rateplan {
       },
     );
 
-    $self->{'__saisei_error'} = "Rate Plan not modified after create"
+    $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
     
   }
@@ -532,6 +624,9 @@ Modify a existing rateplan.
 sub api_modify_existing_rateplan {
   my ($self,$svc,$rateplan_name) = @_;
 
+  $self->{'__saisei_error'} = "There is no download speed set for the service !--service,".$svc->svcnum.",".$rateplan_name."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
+  $self->{'__saisei_error'} = "There is no upload speed set for the service !--service,".$svc->svcnum.",".$rateplan_name."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
+
   my $modified_rateplan = $self->api_call(
     "PUT",
     "/rate_plans/$rateplan_name",
@@ -541,7 +636,7 @@ sub api_modify_existing_rateplan {
     },
   );
 
-    $self->{'__saisei_error'} = "Rate Plan not modified"
+    $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
 
   return;
@@ -565,7 +660,7 @@ sub api_create_user {
       },
   );
 
-  $self->{'__saisei_error'} = "User not created"
+  $self->{'__saisei_error'} = "Saisei could not create the user $user"
     unless ($new_user || $self->{'__saisei_error'}); # should never happen
 
   return $new_user;
@@ -581,7 +676,6 @@ Creates a access point.
 sub api_create_accesspoint {
   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
 
-  # this has not been tested, but should work, if needed.
   my $new_accesspoint = $self->api_call(
       "PUT",
       "/access_points/$accesspoint",
@@ -591,7 +685,7 @@ sub api_create_accesspoint {
       },
   );
 
-  $self->{'__saisei_error'} = "Access point not created"
+  $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
   return;
 
@@ -614,7 +708,7 @@ sub api_modify_accesspoint {
     },
   );
 
-  $self->{'__saisei_error'} = "Rate Plan not modified"
+  $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
 
   return;
@@ -640,7 +734,7 @@ sub api_modify_existing_accesspoint {
     },
   );
 
-    $self->{'__saisei_error'} = "Access point not modified"
+    $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
 
   return;
@@ -666,7 +760,7 @@ sub api_add_host_to_user {
       },
   );
 
-  $self->{'__saisei_error'} = "Host not created"
+  $self->{'__saisei_error'} = "Saisei could not create the host $ip"
     unless ($new_host || $self->{'__saisei_error'}); # should never happen
 
   return $new_host;
@@ -685,7 +779,7 @@ sub api_delete_host_to_user {
 
   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
     return if $self->api_error;
-  $self->{'__saisei_error'} = "Did not receive a default rate plan"
+  $self->{'__saisei_error'} = "Can not delete the host as Saisei did not return a default rate plan. Please make sure Saisei has a default rateplan setup."
     unless $default_rate_plan;
 
   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
@@ -700,7 +794,7 @@ sub api_delete_host_to_user {
       },
   );
 
-  $self->{'__saisei_error'} = "Host not created"
+  $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
 
   return $delete_host;
@@ -710,6 +804,11 @@ sub api_delete_host_to_user {
 sub process_tower {
   my ($self, $opt) = @_;
 
+  if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
+    $self->{'__saisei_error'} = "Could not export tower !--tower,".$opt->{tower_num}.",".$opt->{tower_name}."--! because there was no up or down rates attached to the tower.  Saisei requires a up and down rate be attached to each tower.";
+    return { error => $self->api_error, };
+  }
+
   my $existing_tower_ap;
   my $tower_name = $opt->{tower_name};
 
@@ -722,23 +821,34 @@ sub process_tower {
     '', # tower does not have a uplink on sectors.
     $opt->{tower_uprate_limit},
     $opt->{tower_downrate_limit},
-  ) if $existing_tower_ap && $opt->{modify_existing};
+  ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
 
   #if tower does not exist as an access point create it.
   $self->api_create_accesspoint(
       $tower_name,
       $opt->{tower_uprate_limit},
-      $opt->{tower_downrate_limit}
-  ) unless $existing_tower_ap;
+      $opt->{tower_downrate_limit},
+  ) unless $existing_tower_ap->{collection};
 
   my $accesspoint = $self->api_get_accesspoint($tower_name);
 
+  return { error => $self->api_error, } if $self->api_error;
   return $accesspoint;
 }
 
 sub process_sector {
   my ($self, $opt) = @_;
 
+  if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
+    $self->{'__saisei_error'} = "No sector attached to Tower (".$opt->{tower_name}.") for service ".$opt->{'rateplan'}.".  Saisei requires a tower sector to be attached to each service that is exported to Saisei.";
+    return { error => $self->api_error, };
+  }
+
+  if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
+    $self->{'__saisei_error'} = "Could not export sector !--tower,".$opt->{tower_num}.",".$opt->{sector_name}."--! because there was no up or down rates attached to the sector.  Saisei requires a up and down rate be attached to each sector.";
+    return { error => $self->api_error, };
+  }
+
   my $existing_sector_ap;
   my $sector_name = $opt->{sector_name};
 
@@ -766,6 +876,60 @@ sub process_sector {
   # set access point to existing one or newly created one.
   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
 
+  return { error => $self->api_error, } if $self->api_error;
+  return $accesspoint;
+}
+
+=head2 require_tower_and_sector
+
+sets whether the service export requires a sector with it's tower.
+
+=cut
+
+sub require_tower_and_sector {
+  1;
+}
+
+sub required_fields {
+  my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
+  return @fields;
+}
+
+sub process_virtual_ap {
+  my ($self, $opt) = @_;
+
+  my $existing_virtual_ap;
+  my $virtual_name = $opt->{virtual_name};
+
+  #check if virtual_ap has been set up as an access point.
+  $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
+
+  # modify the existing virtual accesspoint if changing it. this should never happen
+  $self->api_modify_existing_accesspoint (
+    $virtual_name,
+    $opt->{sector_name},
+    $opt->{virtual_uprate_limit},
+    $opt->{virtual_downrate_limit},
+  ) if $existing_virtual_ap && $opt->{modify_existing};
+
+  #if virtual ap does not exist as an access point create it.
+  $self->api_create_accesspoint(
+    $virtual_name,
+    $opt->{virtual_uprate_limit},
+    $opt->{virtual_downrate_limit},
+  ) unless $existing_virtual_ap;
+
+  my $update_sector;
+  if ($existing_virtual_ap && (ref $existing_virtual_ap->{collection}->[0]->{uplink} eq "HASH") && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
+    $update_sector = 1;
+  }
+
+  # Attach newly created virtual ap to tower sector ap or if sector has changed.
+  $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
+
+  # set access point to existing one or newly created one.
+  my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
+
   return $accesspoint;
 }
 
@@ -774,7 +938,7 @@ sub export_provisioned_services {
   my $param = shift;
 
   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
-  or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
+  or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
   bless $part_export;
 
   my @svcparts = FS::Record::qsearch({
@@ -795,13 +959,20 @@ sub export_provisioned_services {
   my $svc_count = scalar @svcs;
 
   my %status = {};
-  for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
+  for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
 
   my $process_count=0;
   foreach my $svc (@svcs) {
     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
     ## check if service exists as host if not export it.
-    _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});
+    my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
+    die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
+    warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
+    my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
+    if ($export_error) {
+      warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
+      die ("$export_error\n");
+    }
     $process_count++;
   }