Add "caas_oam" networking type
[ta/config-manager.git] / cmdatahandlers / src / cmdatahandlers / networking / config.py
1 # Copyright 2019 Nokia
2
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import re
16
17 from cmdatahandlers.api import configerror
18 from cmdatahandlers.api import config
19 from serviceprofiles import profiles
20 from netaddr import IPNetwork, IPSet, IPRange
21
22
23 VALID_NETWORKS = [
24     'caas_oam',
25     'cloud_tenant',
26     'infra_access',
27     'infra_external',
28     'infra_internal',
29     'infra_hw_management',
30     'infra_storage_cluster',
31 ]
32
33 NETWORK_DOMAINS = 'network_domains'
34
35
36 class Config(config.Config):
37     def __init__(self, confman):
38         super(Config, self).__init__(confman)
39         self.ROOT = 'cloud.networking'
40         self.DOMAIN = 'networking'
41         self.freepool = {}
42         self.external_vip = None
43
44     def init(self):
45         if self.ROOT not in self.config:
46             return
47         try:
48             # a mapping between network and free IPSet
49             self.freepool = {}
50             for network in self.config[self.ROOT].keys():
51                 if network in VALID_NETWORKS:
52                     if NETWORK_DOMAINS not in self.config[self.ROOT][network]:
53                         raise configerror.ConfigError('No network domains for network %s' % network)
54
55                     self.freepool[network] = {}
56                     for domain in self.config[self.ROOT][network][NETWORK_DOMAINS].keys():
57                         self.freepool[network][domain] = self._get_free_set(network, domain)
58
59         except configerror.ConfigError:
60             raise
61
62         except Exception as exp:
63             raise configerror.ConfigError(str(exp))
64
65     def _validate_network(self, network, domain=None):
66         networks = self.get_networks()
67         if network not in networks:
68             raise configerror.ConfigError('Invalid network name %s' % network)
69
70         if NETWORK_DOMAINS not in self.config[self.ROOT][network]:
71             raise configerror.ConfigError('No network domains for network %s' % network)
72
73         if domain and domain not in self.config[self.ROOT][network][NETWORK_DOMAINS]:
74             raise configerror.ConfigError('Invalid network domain name %s' % domain)
75
76     def _get_free_set(self, network, domain):
77         ip_range_start = self.get_network_ip_range_start(network, domain)
78         ip_range_end = self.get_network_ip_range_end(network, domain)
79         select_range = IPRange(ip_range_start, ip_range_end)
80         netset = IPSet(select_range)
81         if (network == self.get_infra_external_network_name() and
82                 domain == self._get_vip_domain()):
83             iterator = netset.__iter__()
84             self.external_vip = str(iterator.next())
85             netset.remove(self.external_vip)
86
87         # check for the IP(s) taken by the nodes
88         try:
89             hostsconfig = self.confman.get_hosts_config_handler()
90             hosts = hostsconfig.get_hosts()
91             for host in hosts:
92                 try:
93                     hostip = self.get_host_ip(host, network)
94                     netset.remove(hostip)
95                 except configerror.ConfigError:
96                     pass
97         except configerror.ConfigError:
98             pass
99
100         service_profiles_lib = profiles.Profiles()
101
102         # check for the IP(s) taken as VIPs
103         if network == self.get_infra_internal_network_name() and domain == self._get_vip_domain():
104             vips = self.get_net_vips(network)
105             for _, vip in vips.iteritems():
106                 try:
107                     netset.remove(vip)
108                 except configerror.ConfigError:
109                     pass
110
111         return netset
112
113     def get_dns(self):
114         """ get the list of dns servers
115
116             Return:
117
118             A list of dns servers
119
120             Raise:
121
122             ConfigError is raised in-case of an error
123         """
124         self.validate_root()
125         if 'dns' not in self.config[self.ROOT]:
126             raise configerror.ConfigError('dns not found!')
127
128         return self.config[self.ROOT]['dns']
129
130     def get_mtu(self):
131         """ get the mtu value
132
133             Return:
134
135             A number representing the mtu size
136
137             Raise:
138
139             ConfigError is raised in-case of an error
140         """
141         self.validate_root()
142         if 'mtu' not in self.config['cloud.networking']:
143             raise configerror.ConfigError('mtu not found!')
144         return self.config[self.ROOT]['mtu']
145
146     def get_networks(self):
147         """ get the list of network names
148
149             Return:
150
151             A list of network names
152
153             Raise:
154
155             ConfigError is raised in-case of an error
156         """
157         self.validate_root()
158         networks = []
159         for entry in self.config[self.ROOT]:
160             if entry in VALID_NETWORKS:
161                 networks.append(entry)
162         return networks
163
164     def allocate_ip(self, network, domain):
165         """ get a new free ip in some network
166
167             Arguments:
168
169             Network name
170
171             Network domain
172
173             Return:
174
175             The free ip address
176
177             Raise:
178
179             ConfigError in-case of an error
180         """
181         self._validate_network(network, domain)
182
183         try:
184             iterator = self.freepool[network][domain].__iter__()
185             ip = str(iterator.next())
186             self.freepool[network][domain].remove(ip)
187             return ip
188         except Exception:
189             raise configerror.ConfigError('Failed to allocate ip for network %s in %s' % (network, domain))
190
191     def allocate_static_ip(self, ip, network, domain=None):
192         """ allocate the given ip in some network
193
194             Arguments:
195
196             Ip address
197
198             Network name
199
200             Network domain
201
202             Return:
203
204             The allocated ip address
205
206             Raise:
207
208             ConfigError in-case of an error
209         """
210         self._validate_network(network, domain)
211
212         try:
213             self.freepool[network][domain].remove(ip)
214             return ip
215         except Exception:
216             raise configerror.ConfigError('Failed to allocate %s for network %s in %s' % (ip, network, domain))
217
218     def allocate_vip(self, network):
219         return self.allocate_ip(network, self._get_vip_domain())
220
221     def get_network_domains(self, network):
222         self._validate_network(network)
223         return self.config[self.ROOT][network][NETWORK_DOMAINS].keys()
224
225     def get_network_cidr(self, network, domain):
226         """ get the network cidr
227
228             Arguments:
229
230             Network name
231
232             Network domain
233
234             Return:
235
236             The cidr address
237
238             Raise:
239
240             ConfigError in-case of an error
241         """
242         self._validate_network(network, domain)
243
244         if 'cidr' not in self.config[self.ROOT][network][NETWORK_DOMAINS][domain]:
245             raise configerror.ConfigError('No CIDR for network %s in %s' % (network, domain))
246
247         return self.config[self.ROOT][network][NETWORK_DOMAINS][domain]['cidr']
248
249     def get_vip_network_cidr(self, network):
250         return self.get_network_cidr(network, self._get_vip_domain())
251
252     def get_network_mask(self, network, domain):
253         """ get the network mask
254
255             Arguments:
256
257             Network name
258
259             Network domain
260
261             Return:
262
263             A number representing the mask
264
265             Raise:
266
267             ConfigError in-case of an error
268         """
269         cidr = self.get_network_cidr(network, domain)
270         try:
271             mask = cidr.split('/')[1]
272             return int(mask)
273         except Exception as exp:
274             raise configerror.ConfigError('Invalid network mask in %s: %s' % (cidr, str(exp)))
275
276     def get_vip_network_mask(self, network):
277         return self.get_network_mask(network, self._get_vip_domain())
278
279     def get_network_gateway(self, network, domain):
280         """ get the network gateway
281
282             Arguments:
283
284             Network name
285
286             Network domain
287
288             Return:
289
290             The gateway address
291
292             Raise:
293
294             ConfigError in-case of an error
295         """
296         self._validate_network(network, domain)
297
298         if 'gateway' not in self.config[self.ROOT][network][NETWORK_DOMAINS][domain]:
299             raise configerror.ConfigError('No gateway configured for network %s in %s' % (network, domain))
300
301         return self.config[self.ROOT][network][NETWORK_DOMAINS][domain]['gateway']
302
303     def get_network_routes(self, network, domain):
304         self._validate_network(network, domain)
305
306         if 'routes' not in self.config[self.ROOT][network][NETWORK_DOMAINS][domain]:
307             raise configerror.ConfigError('No routes configured for network %s in %s' % (network, domain))
308
309         return self.config[self.ROOT][network][NETWORK_DOMAINS][domain]['routes']
310
311     def get_network_ip_range_start(self, network, domain):
312         """ get the network allocation range start
313
314             Arguments:
315
316             Network name
317
318             Network domain
319
320             Return:
321
322             The starting allocation range
323
324             Raise:
325
326             ConfigError in-case of an error
327         """
328         net = IPNetwork(self.get_network_cidr(network, domain))
329
330         if 'ip_range_start' in self.config[self.ROOT][network][NETWORK_DOMAINS][domain]:
331             return self.config[self.ROOT][network][NETWORK_DOMAINS][domain]['ip_range_start']
332         else:
333             return str(net[1])
334
335     def get_network_ip_range_end(self, network, domain):
336         """ get the network allocation range end
337
338             Arguments:
339
340             Network name
341
342             Network domain
343
344             Return:
345
346             The end of the allocation range
347
348             Raise:
349
350             ConfigError in-case of an error
351         """
352         net = IPNetwork(self.get_network_cidr(network, domain))
353
354         if 'ip_range_end' in self.config[self.ROOT][network][NETWORK_DOMAINS][domain]:
355             return self.config[self.ROOT][network][NETWORK_DOMAINS][domain]['ip_range_end']
356         else:
357             return str(net[-2])
358
359     def get_network_vlan_id(self, network, domain):
360         """ get the network vlan id
361
362             Arguments:
363
364             Network name
365
366             Network domain
367
368             Return:
369
370             The vlan id
371
372             Raise:
373
374             ConfigError in-case of an error
375         """
376         self._validate_network(network, domain)
377
378         if 'vlan' not in self.config[self.ROOT][network][NETWORK_DOMAINS][domain]:
379             raise configerror.ConfigError('No vlan specified for %s in %s' % (network, domain))
380
381         return self.config[self.ROOT][network][NETWORK_DOMAINS][domain]['vlan']
382
383     def get_vip_network_vlan_id(self, network):
384         return self.get_network_vlan_id(network, self._get_vip_domain())
385
386     def get_network_mtu(self, network):
387         """ get the network mtu
388
389             Argument:
390
391             Network name
392
393             Return:
394
395             The mtu of the network
396
397             Raise:
398
399             ConfigError in-case of an error
400         """
401         self._validate_network(network)
402
403         if 'mtu' not in self.config[self.ROOT][network]:
404             raise configerror.ConfigError('No mtu specified for %s' % network)
405
406         return self.config[self.ROOT][network]['mtu']
407
408     def get_host_ip(self, host, network):
409         """ get the host ip allocated from a specific network
410
411             Argument:
412
413             hostname: The name of the host
414             networkname: The name of the network
415
416             Return:
417
418             The ip address assigned for the host
419
420             Raise:
421
422             ConfigError in-case of an error
423         """
424         self._validate_network(network)
425
426         hostnetconfigkey = host + '.' + self.DOMAIN
427         if hostnetconfigkey not in self.config:
428             raise configerror.ConfigError('No network configuration available for %s' % host)
429
430         if network not in self.config[hostnetconfigkey]:
431             raise configerror.ConfigError('No network configuration available for %s' % host)
432
433         if 'ip' not in self.config[hostnetconfigkey][network]:
434             raise configerror.ConfigError('No IP assigned for %s in network %s' % (host, network))
435
436         return self.config[hostnetconfigkey][network]['ip']
437
438     def _get_vip_domain(self):
439         return self.confman.get_hosts_config_handler().get_managements_network_domain()
440
441     @staticmethod
442     def get_infra_external_network_name():
443         """ get the network name for the external network
444
445             Return:
446
447             The external network name
448
449             Raise:
450
451             ConfigError in-case the network is not configured
452         """
453         return 'infra_external'
454
455     @staticmethod
456     def get_infra_storage_cluster_network_name():
457         """ get the infra storage cluster network name
458
459             Return:
460
461             The infra stroage cluster network name
462
463             Raise:
464
465             ConfigError in-case the network is not configured
466         """
467         return 'infra_storage_cluster'
468
469     @staticmethod
470     def get_hwmgmt_network_name():
471         """ get the hwmgmt network name
472
473             Return:
474
475             The hwmgmt network name
476
477             Raise:
478
479             ConfigError in-case the network is not defined
480         """
481         return 'infra_hw_management'
482
483     @staticmethod
484     def get_infra_internal_network_name():
485         """ get the infra management network name
486
487             Return:
488
489             The infra management network name
490
491             Raise:
492
493             ConfigError in-case the network is not defined
494         """
495         return 'infra_internal'
496
497     @staticmethod
498     def get_caas_oam_network_name():
499         """ get the CaaS OAM network name
500
501             Return:
502
503             The CaaS OAM network name
504
505             Raise:
506
507         """
508         return 'caas_oam'
509
510     def get_cloud_tenant_network_name(self):
511         """ get the network name for the cloud tenant network
512
513             Return:
514
515             The cloud tenant network name
516
517             Raise:
518
519             ConfigError in-case the network is not configured
520         """
521         return 'cloud_tenant'
522
523     def get_infra_access_network_name(self):
524         """ get the network name for the infra access network
525
526             Return:
527
528             The infra access network name
529
530             Raise:
531
532             ConfigError in-case the network is not configured
533         """
534         return 'infra_access'
535
536     def add_host_networks(self, host):
537         """ add host network data
538
539             Argument:
540
541             Host name
542
543             Raise:
544
545             ConfigError in-case of an error
546         """
547         hostsconf = self.confman.get_hosts_config_handler()
548         networks = hostsconf.get_host_networks(host)
549         domain = hostsconf.get_host_network_domain(host)
550         for network in networks:
551             try:
552                 ip = self.get_host_ip(host, network)
553                 continue
554             except configerror.ConfigError:
555                 pass
556
557             static_ip = hostsconf.get_pre_allocated_ips(host, network)
558             if static_ip:
559                 ip = self.allocate_static_ip(static_ip, network, domain)
560             else:
561                 ip = self.allocate_ip(network, domain)
562             interface = hostsconf.get_host_network_ip_holding_interface(host, network)
563             netmask = self.get_network_mask(network, domain)
564             networkdata = {'ip': ip, 'interface': interface, 'mask': netmask}
565
566             try:
567                 vlan = self.get_network_vlan_id(network, domain)
568                 networkdata['vlan'] = vlan
569             except configerror.ConfigError:
570                 pass
571
572             try:
573                 gw = self.get_network_gateway(network, domain)
574                 networkdata['gateway'] = gw
575             except configerror.ConfigError:
576                 pass
577
578             try:
579                 routes = self.get_network_routes(network, domain)
580                 networkdata['routes'] = routes
581             except configerror.ConfigError:
582                 pass
583
584             try:
585                 cidr = self.get_network_cidr(network, domain)
586                 networkdata['cidr'] = cidr
587             except configerror.ConfigError:
588                 pass
589
590             key = host + '.' + self.DOMAIN
591             if key not in self.config:
592                 self.config[key] = {}
593             if network not in self.config[key]:
594                 self.config[key][network] = {}
595
596             self.config[key][network] = networkdata
597
598     def delete_host_networks(self, host):
599         """ delete host network data
600
601             Argument:
602
603             Host name
604         """
605         key = '{}.{}'.format(host, self.DOMAIN)
606         if key in self.config:
607             del self.config[key]
608
609     def get_networking_hosts(self):
610         """ get hosts with networking data
611
612         Return:
613
614         List of host names with existing networking data
615         """
616         hosts = []
617         match = r'^[^.]*\.networking$'
618         for key in self.config.keys():
619             if key != self.ROOT and re.match(match, key):
620                 hosts.append(key.split('.')[0])
621         return hosts
622
623     def get_host_interface(self, host, network):
624         """ get the host interface allocated from a specific network
625
626             Argument:
627
628             hostname: The name of the host
629             networkname: The name of the network
630
631             Return:
632
633             The interface for the host
634
635             Raise:
636
637             ConfigError in-case of an error
638         """
639         self._validate_network(network)
640
641         hostnetconfigkey = host + '.' + self.DOMAIN
642         if hostnetconfigkey not in self.config:
643             raise configerror.ConfigError('No network configuration available for %s' % host)
644
645         if network not in self.config[hostnetconfigkey]:
646             raise configerror.ConfigError('No network configuration available for %s' % host)
647
648         if 'interface' not in self.config[hostnetconfigkey][network]:
649             raise configerror.ConfigError(
650                 'No interface assigned for %s in network %s' % (host, network))
651
652         return self.config[hostnetconfigkey][network]['interface']
653
654     def get_host_mask(self, host, network):
655         """ get the network mask for the host
656
657             Argument:
658
659             hostname: The name of the host
660             networkname: The name of the network
661
662             Return:
663
664             The network mask
665
666             Raise:
667
668             ConfigError in-case of an error
669         """
670         self._validate_network(network)
671
672         hostnetconfigkey = host + '.' + self.DOMAIN
673         if hostnetconfigkey not in self.config:
674             raise configerror.ConfigError('No network configuration available for %s' % host)
675
676         if network not in self.config[hostnetconfigkey]:
677             raise configerror.ConfigError('No network configuration available for %s' % host)
678
679         if 'mask' not in self.config[hostnetconfigkey][network]:
680             raise configerror.ConfigError('No mask assigned for %s in network %s' % (host, network))
681
682         return self.config[hostnetconfigkey][network]['mask']
683
684     def get_external_vip(self):
685         """ get the external vip ip, this is always the first ip in the range
686         """
687         return self.external_vip
688
689     def get_provider_networks(self):
690         """
691         Get provider network names
692
693         Returns:
694             A list of provider network names
695
696         Raises:
697             ConfigError in-case of an error
698         """
699         if 'provider_networks' not in self.config[self.ROOT]:
700             raise configerror.ConfigError('No provider networks configured')
701
702         return self.config[self.ROOT]['provider_networks'].keys()
703
704     def is_shared_provider_network(self, network):
705         """
706         Is shared provider network
707
708         Arguments:
709             Provider network name
710
711         Returns:
712             True if given provider network is shared, False otherwise
713
714         Raises:
715             ConfigError in-case of an error
716         """
717         networks = self.get_provider_networks()
718         if network not in networks:
719             raise configerror.ConfigError('Missing configuration for provider network %s' % network)
720
721         return (self.config[self.ROOT]['provider_networks'][network].get('shared') is True)
722
723     def get_provider_network_vlan_ranges(self, network):
724         """
725         Get vlan ranges for the given provider network
726
727         Arguments:
728             Provider network name
729
730         Returns:
731             Vlan ranges for the provider network
732
733         Raises:
734             ConfigError in-case of an error
735         """
736         networks = self.get_provider_networks()
737         if network not in networks:
738             raise configerror.ConfigError('Missing configuration for provider network %s' % network)
739
740         if 'vlan_ranges' not in self.config[self.ROOT]['provider_networks'][network]:
741             raise configerror.ConfigError(
742                 'Missing vlan ranges configuration for provider network %s' % network)
743
744         return self.config[self.ROOT]['provider_networks'][network]['vlan_ranges']
745
746     def get_provider_network_mtu(self, network):
747         """
748         Get mtu for the given provider network
749
750         Arguments:
751             Provider network name
752
753         Returns:
754             mtu for the provider network
755
756         Raises:
757             ConfigError in-case of an error
758         """
759         networks = self.get_provider_networks()
760         if network not in networks:
761             raise configerror.ConfigError('Missing configuration for provider network %s' % network)
762
763         if 'mtu' not in self.config[self.ROOT]['provider_networks'][network]:
764             raise configerror.ConfigError(
765                 'Missing mtu configuration for provider network %s' % network)
766
767         return self.config[self.ROOT]['provider_networks'][network]['mtu']
768
769     def is_l3_ha_enabled(self):
770         """ is L3 HA enabled
771
772             Return:
773
774             True if L3 HA is enabled, False otherwise
775         """
776         return True if 'l3_ha' in self.config[self.ROOT] else False
777
778     def _get_l3_ha_config(self):
779         if 'l3_ha' not in self.config[self.ROOT]:
780             raise configerror.ConfigError('Missing L3 HA configuration')
781
782         return self.config[self.ROOT]['l3_ha']
783
784     def get_l3_ha_provider_network(self):
785         """ get L3 HA provider network
786
787             Return:
788
789             L3 HA provider network name
790
791             Raise:
792
793             ConfigError in-case of an error
794         """
795         conf = self._get_l3_ha_config()
796         if 'provider_network' not in conf:
797             raise configerror.ConfigError('Missing L3 HA provider network configuration')
798
799         return conf['provider_network']
800
801     def get_l3_ha_cidr(self):
802         """ get L3 HA CIDR
803
804             Return:
805
806             L3 HA CIDR
807
808             Raise:
809
810             ConfigError in-case of an error
811         """
812         conf = self._get_l3_ha_config()
813         if 'cidr' not in conf:
814             raise configerror.ConfigError('Missing L3 HA CIDR configuration')
815
816         return conf['cidr']
817
818     def add_ovs_config_defaults(self, host):
819         """ Add Openvswitch default config """
820
821         ovs_defaults = { 'tx-flush-interval': 0, 'rxq-rebalance': 0 }
822
823         key = self.ROOT
824         if key not in self.config:
825             self.config[key] = {}
826         if 'ovs_config' not in self.config[key]:
827             self.config[key]['ovs_config'] = {}
828         if host not in self.config[key]['ovs_config']:
829             self.config[key]['ovs_config'][host] = {}
830
831         self.config[key]['ovs_config'][host] = ovs_defaults
832
833     def del_ovs_config(self, host):
834         """ Delete Openvswitch config """
835         if host in self.config[self.ROOT]['ovs_config']:
836             self.config[self.ROOT]['ovs_config'].pop(host, None)
837
838     def get_ovs_config(self, host):
839         return self.config[self.ROOT]['ovs_config'].get(host, None)
840
841     def _validate_ovs_config_args(self, host, args):
842         ovs_conf = self.config[self.ROOT]['ovs_config']
843
844         if args.get('tx_flush_interval') is not None:
845             if int(args['tx_flush_interval']) >= 0 and int(args['tx_flush_interval']) <= 1000000:
846                 ovs_conf[host]['tx-flush-interval'] = int(args['tx_flush_interval'])
847             else:
848                 raise ValueError("tx-flush-interval value must be 0..1000000")
849
850         if args.get('rxq_rebalance_interval') is not None:
851             if int(args['rxq_rebalance_interval']) >= 0 and int(args['rxq_rebalance_interval']) <= 1000000:
852                 ovs_conf[host]['rxq-rebalance'] = int(args['rxq_rebalance_interval'])
853             else:
854                 raise ValueError("rxq_rebalance_interval value must be 0..1000000")
855
856     def update_ovs_config(self, host, args):
857         if self.config[self.ROOT]['ovs_config'].get(host, None) is None:
858             return None
859         self._validate_ovs_config_args(host, args)
860         return self.config[self.ROOT]
861
862     def get_ovs_config_hosts(self):
863         return [host for host in self.config[self.ROOT]['ovs_config']]
864
865     def add_vip(self, network, name, ip):
866         if 'vips' not in self.config[self.ROOT]:
867             self.config[self.ROOT]['vips'] = {}
868
869         if network not in self.config[self.ROOT]['vips']:
870             self.config[self.ROOT]['vips'][network] = {}
871
872         self.config[self.ROOT]['vips'][network][name] = ip
873
874     def add_external_vip(self):
875         external_vip = self.get_external_vip()
876         self.add_vip('infra_external', 'external_vip', external_vip)
877
878     def add_internal_vip(self):
879         internal_vip = self.allocate_vip('infra_internal')
880         self.add_vip('infra_internal', 'internal_vip', internal_vip)
881
882     def get_internal_vip(self):
883         try:
884             return self.config[self.ROOT]['vips']['infra_internal']['internal_vip']
885         except KeyError as exp:
886             raise configerror.ConfigError('Internal vip not found')
887
888     def get_vips(self):
889         if 'vips' not in self.config[self.ROOT]:
890             return {}
891
892         return self.config[self.ROOT]['vips']
893
894     def get_net_vips(self, net):
895         if 'vips' not in self.config[self.ROOT]:
896             return {}
897
898         if net not in self.config[self.ROOT]['vips']:
899             return {}
900
901         return self.config[self.ROOT]['vips'][net]
902
903         return self.config[self.ROOT]['vips']