850f16ff4d33159db0e902b8a4d548d56d5a5cae
[so.git] / adapters / mso-adapter-utils / src / main / java / org / onap / so / openstack / utils / MsoNeutronUtils.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Modifications Copyright (c) 2019 Samsung
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  * 
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  * 
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.so.openstack.utils;
24
25
26 import java.util.ArrayList;
27 import java.util.Calendar;
28 import java.util.List;
29 import java.util.Optional;
30
31 import org.onap.so.cloud.CloudConfig;
32 import org.onap.so.cloud.authentication.AuthenticationMethodFactory;
33 import org.onap.so.cloud.authentication.KeystoneAuthHolder;
34 import org.onap.so.cloud.authentication.KeystoneV3Authentication;
35 import org.onap.so.cloud.authentication.ServiceEndpointNotFoundException;
36 import org.onap.so.db.catalog.beans.CloudIdentity;
37 import org.onap.so.db.catalog.beans.CloudSite;
38 import org.onap.so.db.catalog.beans.ServerType;
39 import org.onap.so.logger.ErrorCode;
40 import org.onap.so.logger.MessageEnum;
41 import org.onap.so.openstack.beans.NetworkInfo;
42 import org.onap.so.openstack.exceptions.MsoAdapterException;
43 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
44 import org.onap.so.openstack.exceptions.MsoException;
45 import org.onap.so.openstack.exceptions.MsoIOException;
46 import org.onap.so.openstack.exceptions.MsoNetworkAlreadyExists;
47 import org.onap.so.openstack.exceptions.MsoNetworkNotFound;
48 import org.onap.so.openstack.exceptions.MsoOpenstackException;
49 import org.onap.so.openstack.mappers.NetworkInfoMapper;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52 import org.springframework.beans.factory.annotation.Autowired;
53 import org.springframework.stereotype.Component;
54
55 import com.woorea.openstack.base.client.OpenStackBaseException;
56 import com.woorea.openstack.base.client.OpenStackConnectException;
57 import com.woorea.openstack.base.client.OpenStackRequest;
58 import com.woorea.openstack.base.client.OpenStackResponseException;
59 import com.woorea.openstack.keystone.Keystone;
60 import com.woorea.openstack.keystone.model.Access;
61 import com.woorea.openstack.keystone.model.Authentication;
62 import com.woorea.openstack.keystone.utils.KeystoneUtils;
63 import com.woorea.openstack.quantum.Quantum;
64 import com.woorea.openstack.quantum.model.Network;
65 import com.woorea.openstack.quantum.model.Networks;
66 import com.woorea.openstack.quantum.model.Port;
67 import com.woorea.openstack.quantum.model.Segment;
68
69 @Component
70 public class MsoNeutronUtils extends MsoCommonUtils
71 {
72
73         // Fetch cloud configuration each time (may be cached in CloudConfig class)
74         @Autowired
75         private CloudConfig cloudConfig;
76
77         @Autowired
78     private AuthenticationMethodFactory authenticationMethodFactory;
79         
80         @Autowired
81         private MsoTenantUtilsFactory tenantUtilsFactory;
82
83         @Autowired
84         private KeystoneV3Authentication keystoneV3Authentication;
85
86     private static Logger logger = LoggerFactory.getLogger(MsoNeutronUtils.class);
87
88     public enum NetworkType {
89                 BASIC, PROVIDER, MULTI_PROVIDER
90         };
91
92         /**
93          * Create a network with the specified parameters in the given cloud/tenant.
94          *
95          * If a network already exists with the same name, an exception will be thrown.  Note that
96          * this is an MSO-imposed restriction.  Openstack does not require uniqueness on network names.
97          * <p>
98          * @param cloudSiteId The cloud identifier (may be a region) in which to create the network.
99          * @param tenantId The tenant in which to create the network
100          * @param type The type of network to create (Basic, Provider, Multi-Provider)
101          * @param networkName The network name to create
102          * @param provider The provider network name (for Provider or Multi-Provider networks)
103          * @param vlans A list of VLAN segments for the network (for Provider or Multi-Provider networks)
104          * @return a NetworkInfo object which describes the newly created network
105          * @throws MsoNetworkAlreadyExists Thrown if a network with the same name already exists
106          * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
107          * @throws MsoCloudSiteNotFound Thrown if the cloudSite is invalid or unknown
108          */
109         public NetworkInfo createNetwork (String cloudSiteId, String tenantId, NetworkType type, String networkName, String provider, List<Integer> vlans)
110             throws MsoException
111         {
112                 // Obtain the cloud site information where we will create the stack
113         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
114                 () -> new MsoCloudSiteNotFound(cloudSiteId));
115
116                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
117
118                 // Check if a network already exists with this name
119                 // Openstack will allow duplicate name, so require explicit check
120                 Network network = findNetworkByName (neutronClient, networkName);
121
122                 if (network != null) {
123                         // Network already exists.  Throw an exception
124         logger.error("{} Network {} on Cloud site {} for tenant {} already exists {}",
125             MessageEnum.RA_NETWORK_ALREADY_EXIST, networkName, cloudSiteId, tenantId,
126             ErrorCode.DataError.getValue());
127         throw new MsoNetworkAlreadyExists (networkName, tenantId, cloudSiteId);
128                 }
129
130                 // Does not exist, create a new one
131                 network = new Network();
132                 network.setName(networkName);
133                 network.setAdminStateUp(true);
134
135                 if (type == NetworkType.PROVIDER) {
136                         if (provider != null && vlans != null && vlans.size() > 0) {
137                                 network.setProviderPhysicalNetwork (provider);
138                                 network.setProviderNetworkType("vlan");
139                                 network.setProviderSegmentationId (vlans.get(0));
140                         }
141                 } else if (type == NetworkType.MULTI_PROVIDER) {
142                         if (provider != null && vlans != null && vlans.size() > 0) {
143                                 List<Segment> segments = new ArrayList<>(vlans.size());
144                                 for (int vlan : vlans) {
145                                         Segment segment = new Segment();
146                                         segment.setProviderPhysicalNetwork (provider);
147                                         segment.setProviderNetworkType("vlan");
148                                         segment.setProviderSegmentationId (vlan);
149
150                                         segments.add(segment);
151                                 }
152                                 network.setSegments(segments);
153                         }
154                 }
155
156                 try {
157                         OpenStackRequest<Network> request = neutronClient.networks().create(network);
158                         Network newNetwork = executeAndRecordOpenstackRequest(request);
159                         return new NetworkInfoMapper(newNetwork).map();
160                 }
161                 catch (OpenStackBaseException e) {
162                         // Convert Neutron exception to an MsoOpenstackException
163                         MsoException me = neutronExceptionToMsoException (e, "CreateNetwork");
164                         throw me;
165                 }
166                 catch (RuntimeException e) {
167                         // Catch-all
168                         MsoException me = runtimeExceptionToMsoException(e, "CreateNetwork");
169                         throw me;
170                 }
171         }
172
173
174         /**
175          * Query for a network with the specified name or ID in the given cloud.  If the network exists,
176          * return an NetworkInfo object.  If not, return null.
177          * <p>
178          * Whenever possible, the network ID should be used as it is much more efficient.  Query by
179          * name requires retrieval of all networks for the tenant and search for matching name.
180          * <p>
181          * @param networkNameOrId The network to query
182          * @param tenantId The Openstack tenant to look in for the network
183          * @param cloudSiteId The cloud identifier (may be a region) in which to query the network.
184          * @return a NetworkInfo object describing the queried network, or null if not found
185          * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
186          * @throws MsoCloudSiteNotFound
187          */
188     public NetworkInfo queryNetwork(String networkNameOrId, String tenantId, String cloudSiteId) throws MsoException
189         {
190       logger.debug("In queryNetwork");
191
192                 // Obtain the cloud site information
193         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
194                 () -> new MsoCloudSiteNotFound(cloudSiteId));
195
196                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
197
198                 // Check if the network exists and return its info
199                 try {
200                         Network network = findNetworkByNameOrId (neutronClient, networkNameOrId);
201                         if (network == null) {
202           logger.debug("Query Network: {} not found in tenant {}", networkNameOrId, tenantId);
203           return null;
204                         }
205                         return new NetworkInfoMapper(network).map();
206                 }
207                 catch (OpenStackBaseException e) {
208                         // Convert Neutron exception to an MsoOpenstackException
209                         MsoException me = neutronExceptionToMsoException (e, "QueryNetwork");
210                         throw me;
211                 }
212                 catch (RuntimeException e) {
213                         // Catch-all
214                         MsoException me = runtimeExceptionToMsoException(e, "QueryNetwork");
215                         throw me;
216                 }
217         }
218     
219     public Optional<Port> getNeutronPort(String neutronPortId, String tenantId, String cloudSiteId)
220         {
221                 try {
222                         logger.debug("Finding Neutron port:" + neutronPortId);
223                           CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
224                                 () -> new MsoCloudSiteNotFound(cloudSiteId));
225                                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
226                         Port port = findPortById (neutronClient, neutronPortId);
227                         if (port == null) {                             
228                                 return Optional.empty();
229                         }
230                         return Optional.of(port);
231                 }
232                 catch (RuntimeException | MsoException e) {
233                         logger.error("Error retrieving neutron port", e);
234                         return Optional.empty();
235                 }
236         }
237
238         /**
239          * Delete the specified Network (by ID) in the given cloud.
240          * If the network does not exist, success is returned.
241          * <p>
242          * @param networkId Openstack ID of the network to delete
243          * @param tenantId The Openstack tenant.
244          * @param cloudSiteId The cloud identifier (may be a region) from which to delete the network.
245          * @return true if the network was deleted, false if the network did not exist
246          * @throws MsoOpenstackException If the Openstack API call returns an exception, this local
247          * exception will be thrown.
248          * @throws MsoCloudSiteNotFound
249          */
250     public boolean deleteNetwork(String networkId, String tenantId, String cloudSiteId) throws MsoException
251         {
252                 // Obtain the cloud site information where we will create the stack
253         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
254                 () -> new MsoCloudSiteNotFound(cloudSiteId));
255                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
256
257                 try {
258                         // Check that the network exists.
259                         Network network = findNetworkById (neutronClient, networkId);
260                         if (network == null) {
261           logger.info("{} Network not found! Network id: {} Cloud site: {} Tenant: {} ",
262               MessageEnum.RA_DELETE_NETWORK_EXC, networkId, cloudSiteId, tenantId);
263           return false;
264                         }
265
266                         OpenStackRequest<Void> request = neutronClient.networks().delete(network.getId());
267                         executeAndRecordOpenstackRequest(request);
268
269         logger.debug("Deleted Network {} ({})", network.getId(), network.getName());
270     }
271                 catch (OpenStackBaseException e) {
272                         // Convert Neutron exception to an MsoOpenstackException
273                         MsoException me = neutronExceptionToMsoException (e, "Delete Network");
274                         throw me;
275                 }
276                 catch (RuntimeException e) {
277                         // Catch-all
278                         MsoException me = runtimeExceptionToMsoException(e, "DeleteNetwork");
279                         throw me;
280                 }
281
282                 return true;
283         }
284
285
286         /**
287          * Update a network with the specified parameters in the given cloud/tenant.
288          *
289          * Specifically, this call is intended to update the VLAN segments on a
290          * multi-provider network.  The provider segments will be replaced with the
291          * supplied list of VLANs.
292          * <p>
293          * Note that updating the 'segments' array is not normally supported by Neutron.
294          * This method relies on a Platform Orchestration extension (using SDN controller
295          * to manage the virtual networking).
296          *
297          * @param cloudSiteId The cloud site ID (may be a region) in which to update the network.
298          * @param tenantId Openstack ID of the tenant in which to update the network
299          * @param networkId The unique Openstack ID of the network to be updated
300          * @param type The network type (Basic, Provider, Multi-Provider)
301          * @param provider The provider network name.  This should not change.
302          * @param vlans The list of VLAN segments to replace
303          * @return a NetworkInfo object which describes the updated network
304          * @throws MsoNetworkNotFound Thrown if the requested network does not exist
305          * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
306          * @throws MsoCloudSiteNotFound
307          */
308         public NetworkInfo updateNetwork (String cloudSiteId, String tenantId, String networkId, NetworkType type, String provider, List<Integer> vlans)
309             throws MsoException
310         {
311                 // Obtain the cloud site information where we will create the stack
312         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
313                 () -> new MsoCloudSiteNotFound(cloudSiteId));
314                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
315
316                 // Check that the network exists
317                 Network network = findNetworkById (neutronClient, networkId);
318
319                 if (network == null) {
320                         // Network not found.  Throw an exception
321         logger.error("{} Network {} on Cloud site {} for Tenant {} not found {}", MessageEnum.RA_NETWORK_NOT_FOUND,
322             networkId, cloudSiteId, tenantId, ErrorCode.DataError.getValue());
323                         throw new MsoNetworkNotFound (networkId, tenantId, cloudSiteId);
324                 }
325
326                 // Overwrite the properties to be updated
327                 if (type == NetworkType.PROVIDER) {
328                         if (provider != null && vlans != null && vlans.size() > 0) {
329                                 network.setProviderPhysicalNetwork (provider);
330                                 network.setProviderNetworkType("vlan");
331                                 network.setProviderSegmentationId (vlans.get(0));
332                         }
333                 } else if (type == NetworkType.MULTI_PROVIDER) {
334                         if (provider != null && vlans != null && vlans.size() > 0) {
335                                 List<Segment> segments = new ArrayList<>(vlans.size());
336                                 for (int vlan : vlans) {
337                                         Segment segment = new Segment();
338                                         segment.setProviderPhysicalNetwork (provider);
339                                         segment.setProviderNetworkType("vlan");
340                                         segment.setProviderSegmentationId (vlan);
341
342                                         segments.add(segment);
343                                 }
344                                 network.setSegments(segments);
345                         }
346                 }
347
348                 try {
349                         OpenStackRequest<Network> request = neutronClient.networks().update(network);
350                         Network newNetwork = executeAndRecordOpenstackRequest(request);
351                         return new NetworkInfoMapper(newNetwork).map();
352                 }
353                 catch (OpenStackBaseException e) {
354                         // Convert Neutron exception to an MsoOpenstackException
355                         MsoException me = neutronExceptionToMsoException (e, "UpdateNetwork");
356                         throw me;
357                 }
358                 catch (RuntimeException e) {
359                         // Catch-all
360                         MsoException me = runtimeExceptionToMsoException(e, "UpdateNetwork");
361                         throw me;
362                 }
363         }
364
365
366         // -------------------------------------------------------------------
367         // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS
368
369         /**
370          * Get a Neutron (Quantum) client for the Openstack Network service.
371          * This requires a 'member'-level userId + password, which will be retrieved from
372          * properties based on the specified cloud Id.  The tenant in which to operate
373          * must also be provided.
374          * <p>
375          * On successful authentication, the Quantum object will be cached for the
376          * tenantID + cloudId so that it can be reused without reauthenticating with
377          *  Openstack every time.
378          *
379          * @param cloudSite - a cloud site definition
380          * @param tenantId - Openstack tenant ID
381          * @return an authenticated Quantum object
382          */
383     private Quantum getNeutronClient(CloudSite cloudSite, String tenantId) throws MsoException
384         {
385                 String cloudId = cloudSite.getId();
386                 String region = cloudSite.getRegionId();        
387
388
389                 // Obtain an MSO token for the tenant from the identity service
390                 CloudIdentity cloudIdentity = cloudSite.getIdentityService();
391                 MsoTenantUtils tenantUtils = tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType());
392         final String keystoneUrl = tenantUtils.getKeystoneUrl(cloudId, cloudIdentity);
393                 String neutronUrl = null;
394                 String tokenId = null;
395                 Calendar expiration = null;
396                 try {
397                 if (ServerType.KEYSTONE.equals(cloudIdentity.getIdentityServerType())) {
398                                 Keystone keystoneTenantClient = new Keystone(keystoneUrl);
399                                 Access access = null;
400                                 
401                                 Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity);
402                                 OpenStackRequest<Access> request = keystoneTenantClient.tokens().authenticate(credentials).withTenantId(tenantId);
403                                 access = executeAndRecordOpenstackRequest(request);
404                                 
405                                 
406                                 try {
407                                         neutronUrl = KeystoneUtils.findEndpointURL(access.getServiceCatalog(), "network", region, "public");
408                                         if (! neutronUrl.endsWith("/")) {
409                                 neutronUrl += "/v2.0/";
410                             }
411                                 } catch (RuntimeException e) {
412                                         // This comes back for not found (probably an incorrect region ID)
413                                         String error = "Network service not found: region=" + region + ",cloud=" + cloudIdentity.getId();
414                                         throw new MsoAdapterException (error, e);
415                                 }
416                                 tokenId = access.getToken().getId();
417                                 expiration = access.getToken().getExpires();
418                 } else if (ServerType.KEYSTONE_V3.equals(cloudIdentity.getIdentityServerType())) {
419                         try {
420                                 KeystoneAuthHolder holder = keystoneV3Authentication.getToken(cloudSite, tenantId, "network");
421                                 tokenId = holder.getId();
422                                 expiration = holder.getexpiration();
423                                 neutronUrl = holder.getServiceUrl();
424                                 if (! neutronUrl.endsWith("/")) {
425                                 neutronUrl += "/v2.0/";
426                             }
427                         } catch (ServiceEndpointNotFoundException e) {
428                                 // This comes back for not found (probably an incorrect region ID)
429                                         String error = "Network service not found: region=" + region + ",cloud=" + cloudIdentity.getId();
430                                         throw new MsoAdapterException (error, e);
431                         }
432                 }
433                 }
434                 catch (OpenStackResponseException e) {
435                         if (e.getStatus() == 401) {
436                                 // Authentication error.
437                                 String error = "Authentication Failure: tenant=" + tenantId + ",cloud=" + cloudIdentity.getId();
438
439                                 throw new MsoAdapterException(error);
440                         }
441                         else {
442                                 MsoException me = keystoneErrorToMsoException(e, "TokenAuth");
443                                 throw me;
444                         }
445                 }
446                 catch (OpenStackConnectException e) {
447                         // Connection to Openstack failed
448                         MsoIOException me = new MsoIOException (e.getMessage(), e);
449                         me.addContext("TokenAuth");
450                         throw me;
451                 }
452                 catch (RuntimeException e) {
453                         // Catch-all
454                         MsoException me = runtimeExceptionToMsoException(e, "TokenAuth");
455                         throw me;
456                 }
457
458                 Quantum neutronClient = new Quantum(neutronUrl);
459                 neutronClient.token(tokenId);
460                 return neutronClient;
461         }
462
463         /*
464          * Find a tenant (or query its existence) by its Name or Id.  Check first against the
465          * ID.  If that fails, then try by name.
466          *
467          * @param adminClient an authenticated Keystone object
468          * @param tenantName the tenant name or ID to query
469          * @return a Tenant object or null if not found
470          */
471         public Network findNetworkByNameOrId (Quantum neutronClient, String networkNameOrId)
472         {
473                 if (networkNameOrId == null) {
474             return null;
475         }
476
477                 Network network = findNetworkById(neutronClient, networkNameOrId);
478
479                 if (network == null) {
480             network = findNetworkByName(neutronClient, networkNameOrId);
481         }
482
483                 return network;
484         }
485
486         /*
487          * Find a network (or query its existence) by its Id.
488          *
489          * @param neutronClient an authenticated Quantum object
490          * @param networkId the network ID to query
491          * @return a Network object or null if not found
492          */
493         private Network findNetworkById (Quantum neutronClient, String networkId)
494         {
495                 if (networkId == null) {
496             return null;
497         }
498
499                 try {
500                         OpenStackRequest<Network> request = neutronClient.networks().show(networkId);
501                         Network network = executeAndRecordOpenstackRequest(request);
502                         return network;
503                 }
504                 catch (OpenStackResponseException e) {
505                         if (e.getStatus() == 404) {
506                                 return null;
507                         } else {
508           logger.error("{} {} Openstack Error, GET Network By ID ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
509               ErrorCode.DataError.getValue(), networkId, e);
510           throw e;
511                         }
512                 }
513         }
514         
515         
516         private Port findPortById (Quantum neutronClient, String neutronPortId)
517         {
518                 if (neutronPortId == null) {
519             return null;
520         }
521
522                 try {
523                         OpenStackRequest<Port> request = neutronClient.ports().show(neutronPortId);
524                         Port port = executeAndRecordOpenstackRequest(request);
525                         return port;
526                 }
527                 catch (OpenStackResponseException e) {
528                         if (e.getStatus() == 404) {
529                                 logger.warn("Neutron port not found: " + neutronPortId,"Neutron port not found: " + neutronPortId);
530                                 return null;
531                         } else {
532                                 logger.error("{} {} Openstack Error, GET Neutron Port By ID ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
533                                         ErrorCode.DataError.getValue(), neutronPortId, e);
534                                 throw e;
535                         }
536                 }
537         }
538
539         /*
540          * Find a network (or query its existence) by its Name.  This method avoids an
541          * initial lookup by ID when it's known that we have the network Name.
542          *
543          * Neutron does not support 'name=*' query parameter for Network query (show).
544          * The only way to query by name is to retrieve all networks and look for the
545          * match.  While inefficient, this capability will be provided as it is needed
546          * by MSO, but should be avoided in favor of ID whenever possible.
547          *
548          * TODO:
549          * Network names are not required to be unique, though MSO will attempt to enforce
550          * uniqueness.  This call probably needs to return an error (instead of returning
551          * the first match).
552          *
553          * @param neutronClient an authenticated Quantum object
554          * @param networkName the network name to query
555          * @return a Network object or null if not found
556          */
557         public Network findNetworkByName (Quantum neutronClient, String networkName)
558         {
559                 if (networkName == null) {
560             return null;
561         }
562
563                 try {
564                         OpenStackRequest<Networks> request = neutronClient.networks().list();
565                         Networks networks = executeAndRecordOpenstackRequest(request);
566                         for (Network network : networks.getList()) {
567                                 if (network.getName().equals(networkName)) {
568             logger.debug("Found match on network name: {}", networkName);
569             return network;
570                                 }
571                         }
572         logger.debug("findNetworkByName - no match found for {}", networkName);
573         return null;
574                 }
575                 catch (OpenStackResponseException e) {
576                         if (e.getStatus() == 404) {
577                                 return null;
578                         } else {
579           logger.error("{} {} Openstack Error, GET Network By Name ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
580               ErrorCode.DataError.getValue(), networkName, e);
581           throw e;
582                         }
583                 }
584         }
585 }