-
Notifications
You must be signed in to change notification settings - Fork 2
/
xr-compose
executable file
·1982 lines (1602 loc) · 61.5 KB
/
xr-compose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# ------------------------------------------------------------------------------
# xr-compose - Translates 'XR-YAML' into full docker-compose YAML to be used
# for launching topologies of XR Docker containers
#
# Copyright (c) 2020-2022 by Cisco Systems, Inc.
# All rights reserved.
# ------------------------------------------------------------------------------
"""
Translates 'XR-YAML' into full docker-compose YAML to be used for launching
topologies of XR Docker containers
"""
import argparse
import os
import sys
import logging
import subprocess
import enum
import re
import typing
from collections import OrderedDict
from typing import Any, Dict, Iterable, List, Optional, Union
import yaml
# --------------
# Global variables
# --------------
LOGGER = logging.getLogger()
# Defualt input and output files
DEFAULT_INPUT = "docker-compose.xr.yml"
DEFAULT_OUTPUT = "docker-compose.yml"
# Default docker-compose YAML version if unspecified in the input file.
DEFAULT_YAML_VERSION = "2.4"
# Valid interface name strings
GI_IF_RE = r"Gi0/0/0/(\d+)"
MG_IF_RE = r"Mg0/RP0/CPU0/(\d+)"
# Env vars
STARTUP_CONFIG_ENV = "XR_EVERY_BOOT_CONFIG"
BOOT_SCRIPT_ENV = "XR_EVERY_BOOT_SCRIPT"
INTERFACES_ENV = "XR_INTERFACES"
MGMT_INTERFACES_ENV = "XR_MGMT_INTERFACES"
# Paths inside container
STARTUP_CONFIG_PATH = "/etc/xrd/startup.cfg"
BOOT_SCRIPT_PATH = "/etc/xrd/boot_script"
PROVISION_VOLUME_PATH = "/xr-storage/"
# XR YAML keywords that may be included in input YAML
NON_XR = "non_xr"
XR_KEYWORD_PREFIX = "xr_"
XR_STARTUP_CFG = "xr_startup_cfg"
XR_BOOT_SCRIPT = "xr_boot_script"
XR_INTERFACES = "xr_interfaces"
XR_CHECKSUM_INTERFACES = "xr_checksum_interfaces"
XR_SNOOP_V4_INTERFACES = "xr_snoop_v4_interfaces"
XR_SNOOP_V6_INTERFACES = "xr_snoop_v6_interfaces"
XR_SNOOP_V4_DEFAULT_ROUTE_INTERFACES = "xr_snoop_v4_default_route"
XR_SNOOP_V6_DEFAULT_ROUTE_INTERFACES = "xr_snoop_v4_default_route"
XR_L2NETWORKS = "xr_l2networks"
# Lists of XR keywords that are valid on particular YAML constructs
VALID_XR_SERVICE_KEYWORDS = [
XR_INTERFACES,
XR_STARTUP_CFG,
XR_BOOT_SCRIPT,
XR_CHECKSUM_INTERFACES,
XR_SNOOP_V4_INTERFACES,
XR_SNOOP_V6_INTERFACES,
XR_SNOOP_V4_DEFAULT_ROUTE_INTERFACES,
XR_SNOOP_V6_DEFAULT_ROUTE_INTERFACES,
]
VALID_NETWORK_KEYWORDS = [XR_INTERFACES]
VALID_TOP_LEVEL_KEYWORDS = [XR_L2NETWORKS]
# --------------
# Exceptions
# --------------
class Error(Exception):
"""
Errors for the XR compose script
"""
class InputDoesNotExistError(Error):
"""
The input file does not exist.
"""
def __init__(self, input_file: str):
super().__init__()
self.input_file = input_file
def __str__(self) -> str:
return "Input file {} does not exist!".format(self.input_file)
class DockerObjectsRemainingError(Error):
"""
Docker objects are still running that would be left unmanaged if the ouptut
file was overwritten
"""
def __init__(
self,
containers: List[str],
networks: List[str],
volumes: List[str],
output_file: str,
):
super().__init__()
self.containers = containers
self.networks = networks
self.volumes = volumes
self.output_file = output_file
def __str__(self) -> str:
return (
"The following Docker objects referred to by the output file "
"{} are still running:\n"
"Containers:\n {}\n"
"Named networks:\n {}\n"
"Volumes:\n {}\n".format(
self.output_file,
"\n ".join(self.containers),
"\n ".join(self.networks),
"\n ".join(self.volumes),
)
)
class InputYAMLXRError(Error):
"""
The input YAML contained errors with XR keywords and constructs.
"""
def __init__(self, input_file: str, errors: List[Exception]):
super().__init__()
self.input_file = input_file
self.errors = errors
def __str__(self) -> str:
return "Input file {} contains the following errors:\n {}".format(
self.input_file,
"\n ".join([str(error) for error in self.errors]),
)
class NonXRServiceXRKeywordError(Error):
"""
A service marked as non-XR contains an XR keyword.
"""
def __init__(self, service_name: str):
super().__init__()
self.service_name = service_name
def __str__(self) -> str:
return (
"Non-XR service {} contains an keyword starting with "
"'xr_'.".format(self.service_name)
)
class InvalidXRKeywordError(Error):
"""
An XR service contains an invalid XR keyword.
"""
def __init__(self, object_name: str, xr_keyword: str):
super().__init__()
self.object_name = object_name
self.xr_keyword = xr_keyword
def __str__(self) -> str:
return "Compose object {} contains an invalid XR keyword {}.".format(
self.object_name, self.xr_keyword
)
class InvalidXRInterfaceError(Error):
"""
An XR Interface has an invalid name.
"""
def __init__(
self, interface_name: str, interface: Optional["Interface"] = None
):
super().__init__()
self.interface_name = interface_name
self.interface = interface
def __str__(self) -> str:
return "Invalid interface name {}.".format(self.interface_name)
class DuplicateInterfaceError(Error):
"""
An XR service has duplicate interfaces
"""
def __init__(self, service_name: str, interface_name: str):
super().__init__()
self.interface_name = interface_name
self.service_name = service_name
def __str__(self) -> str:
return "Service {} has a duplicate interface {}.".format(
self.service_name, self.interface_name
)
class XRL2NetworkNonListError(Error):
"""
The XR L2Networks section is not a list
"""
def __str__(self) -> str:
return (
"The XR L2Networks section must contain a list of lists of "
"interfaces that define separate networks."
)
class NonExistentServiceError(Error):
"""
A network section mentions a nonexistent service
"""
def __init__(self, network_name: str, service_name: str):
super().__init__()
self.network_name = network_name
self.service_name = service_name
def __str__(self) -> str:
return "{} mentions service {} that does not exist.".format(
self.network_name, self.service_name
)
class NonExistentInterfaceError(Error):
"""
A network section mentions a nonexistent interface
"""
def __init__(
self, network_section: str, service_name: str, interface_name: str
):
super().__init__()
self.network_section = network_section
self.service_name = service_name
self.interface_name = interface_name
def __str__(self) -> str:
return (
"The {} section mentions interface {} that does not exist on "
"service {}.".format(
self.network_section, self.interface_name, self.service_name
)
)
class DoubleNetworkError(Error):
"""
An interface is included in more than one network
"""
def __init__(self, interface_name: str):
super().__init__()
self.interface_name = interface_name
def __str__(self) -> str:
return (
"The interface {} is mentioned in more than one "
"network.".format(self.interface_name)
)
class XRNetworkInterfacesNonListError(Error):
"""
The XR interfaces section in an XR network is not a list
"""
def __init__(self, network_name: str):
super().__init__()
self.network_name = network_name
def __str__(self) -> str:
return (
"The XR interfaces section in network {} must contain a list "
"of interfaces.".format(self.network_name)
)
class XRServiceInterfacesNonListError(Error):
"""
The XR interfaces section in an XR service is not a list
"""
def __init__(self, service_name: str):
super().__init__()
self.service_name = service_name
def __str__(self) -> str:
return (
"The XR interfaces section in service {} must contain a list "
"of interfaces.".format(self.service_name)
)
class XRStartupConfigNonStringError(Error):
"""
The XR startup config is not a string
"""
def __init__(self, service_name: str):
super().__init__()
self.service_name = service_name
def __str__(self) -> str:
return (
"The XR startup config section in service {} must only "
"contain the path to the startup config file.".format(
self.service_name
)
)
class EnvVarExistsError(Error):
"""
An env var that should be generated already exists
"""
def __init__(self, env_var: str):
super().__init__()
self.env_var = env_var
def __str__(self) -> str:
return (
"The environment variable {} is generated by this script and "
"may not be specified.".format(self.env_var)
)
class VolumeExistsError(Error):
"""
An env var that should be generated already exists
"""
def __init__(self, volume_name: str):
super().__init__()
self.volume_name = volume_name
def __str__(self) -> str:
return (
"The volume {} is generated by this script and "
"may not be specified.".format(self.volume_name)
)
class NetworkExistsError(Error):
"""
A network that should be generated already exists
"""
def __init__(self, network_name: str):
super().__init__()
self.network_name = network_name
def __str__(self) -> str:
return (
"The network {} is generated by this script and "
"may not be specified.".format(self.network_name)
)
class MissingImageError(Error):
"""
A service has no appopriate image to use - none was specified in YAML or
in the CLI
"""
def __init__(self, service_name: str):
super().__init__()
self.service_name = service_name
def __str__(self) -> str:
return (
"No image was specified for service {}.\nImages may be specified "
"via:\n"
" - -i/--image CLI argument (see --help usage)\n"
" - Using the image keyword to specify images individually in "
"input YAML".format(self.service_name)
)
class InputYAMLError(Error):
"""
There was a YAML error in the input.
"""
def __init__(self, error_str: str):
super().__init__()
self.error_str = error_str
def __str__(self) -> str:
return "The input YAML contains syntax errors: {}".format(
self.error_str
)
# --------------
# Classes
# --------------
class InterfaceType(enum.Enum):
"""
Enumeration of the possible interface types
"""
GI = "GigabitEthernet"
MGMT = "MgmtEth"
class ChecksumOffload(enum.Enum):
"""
Enumeration of possible checksum offload states for an interface
"""
UNDETERMINED = "Undetermined"
ENABLED = "Enabled"
DISABLED = "Disabled"
class Interface:
"""
Class to represent an interface in an XR container
Attributes:
name:
Name of the interface
name_abbrev:
Abbreviated name to use in Docker network object names.
network:
Network instance that this interface belongs to
service:
Service instance that this interface belongs to
This will be filled in when an Interface is added to a service.
type:
Enum representing the type of interface
port:
Port number for GigabitEthernet interfaces
chksum_offload:
Enum representing whether checksum offload counteract should be
enabled or disabled for this service, or whether it is undecided.
snoop_v4:
Bool indicating whether or not XRd should snoop the IPv4 address of
the interface and add corresponding XR config.
snoop_v6:
Bool indicating whether or not XRd should snoop the IPv6 address of
the interface and add corresponding XR config.
snoop_v4_default_route:
Bool indicating whether or not XRd should snoop the IPv4 default
route of the interface and add corresponding XR config.
snoop_v6_default_route:
Bool indicating whether or not XRd should snoop the IPv4 default
route of the interface and add corresponding XR config.
"""
def __init__(self, interface: Union[str, Dict[str, Dict[str, bool]]]):
"""
Initialises an Interface instance and sanitises the name
:param interface:
Either the XR name of the interface, or a dictionary mapping
XR interface name to a dictionary of flags and their boolean values.
Raises:
InvalidXRInterfaceError
The interface name is not valid
"""
self.chksum_offload: ChecksumOffload = ChecksumOffload.UNDETERMINED
self.snoop_v4: bool = False
self.snoop_v6: bool = False
self.snoop_v4_default_route: bool = False
self.snoop_v6_default_route: bool = False
if isinstance(interface, str):
self.name = interface
else:
interface_keys = interface.keys()
if len(interface_keys) != 1:
raise ValueError(
f"Interface {interface} must have exactly one name."
)
self.name = list(interface_keys)[0]
flags = interface[self.name]
for flag, value in flags.items():
if flag == "chksum":
self.chksum_offload = (
ChecksumOffload.ENABLED
if value
else ChecksumOffload.DISABLED
)
elif flag in {
"snoop_v4",
"snoop_v6",
"snoop_v4_default_route",
"snoop_v6_default_route",
}:
setattr(self, flag, value)
self.name_abbrev: Optional[str] = None
self.network: Optional[Network] = None
self.service: Optional[Service] = None
self.type: Optional[InterfaceType] = None
self.port: Optional[str] = None
def sanitise(self) -> None:
"""
Checks whether the interface name is valid, and generates
a name abbreviation to use for Docker network object names
Raises:
InvalidXRInterfaceError
The interface name is not valid
"""
if self.name is None:
raise InvalidXRInterfaceError("None")
for exp, iftype, abbrev in (
(GI_IF_RE, InterfaceType.GI, "gi"),
(MG_IF_RE, InterfaceType.MGMT, "mg"),
):
match = re.match(exp, self.name)
if match:
self.type = iftype
self.port = match.group(1)
self.name_abbrev = "{}{}".format(abbrev, self.port)
if self.type is None:
raise InvalidXRInterfaceError(self.name, self)
@property
def is_mgmt(self) -> bool:
return self.name.startswith("Mg")
@property
def flags(self) -> List[str]:
flags = [f"xr_name={self.name}"]
if self.chksum_offload is ChecksumOffload.ENABLED:
flags.append("chksum")
if self.snoop_v4:
flags.append("snoop_v4")
if self.snoop_v6:
flags.append("snoop_v6")
if self.snoop_v4_default_route:
flags.append("snoop_v4_default_route")
if self.snoop_v6_default_route:
flags.append("snoop_v6_default_route")
return flags
def generate_topo_id_name(topo_id: Optional[str], base_name: str) -> str:
"""
Append the topology ID to the name '<name>-<topo_id>'. If there is
no topology ID then simply return the name.
Arguments:
topo_id:
Topology ID to be suffixed to the container name (may be None)
base_name:
The name of the object without the topology identified.
"""
if topo_id is not None:
return "{}-{}".format(base_name, topo_id)
return base_name
class Service:
"""
Class to represent an XR container service:
Attributes:
service_name:
Name of the service from the input YAML
(Note: not the same as the container name)
container_name:
Name that will be given to the running Docker container
interface_dict:
Ordered dictionary of interface name to Interface instance
that this service contains
startup_cfg:
Path to startup config that should be used for this XR
container service
boot_script:
Path to boot script that should be used for this XR
container service
"""
def __init__(
self,
service_name: str,
topo_id: Optional[str],
startup_cfg: Optional[str] = None,
boot_script: Optional[str] = None,
interfaces: Optional[List[Interface]] = None,
):
"""
Initialises a Service instance.
Arguments:
service_name:
Name of the service
topo_id:
Topology ID to be suffixed to the container name
startup_cfg:
Path to startup config for this service
boot_script:
Path to boot script that should be used for this XR
container service
interfaces:
List of Interface instances
"""
self.topo_id = topo_id
self.service_name = service_name
self.container_name = generate_topo_id_name(topo_id, self.service_name)
self.startup_cfg = startup_cfg
self.boot_script = boot_script
self.interface_dict = (
OrderedDict((i.name, i) for i in interfaces)
if interfaces
else OrderedDict()
)
for interface in self.interfaces:
LOGGER.debug(
"Adding interface %s to service %s",
interface.name,
self.service_name,
)
interface.service = self
def get_interface(self, interface_name: str) -> Optional[Interface]:
"""
Returns the interface with the given name, or none if it doesn't exist
"""
return self.interface_dict.get(interface_name)
@property
def interfaces(self) -> Iterable[Interface]:
"""
Used to iterate over the Interface instances in this service
"""
return self.interface_dict.values()
class Network:
"""
Class to represent a network of XR containers
Class variables:
network_id:
Incremented for each network created, and
used to create unique container interface
prefixes
Attributes:
name:
Name of the network
interfaces:
List of Interface instances that are in this network
generated:
Boolean indicating whether this network existed input YAML
or has been generated from the xr_l2networks section, or for
a lone XR interface.
container_prefix:
Prefix used for interfaces in this network inside containers.
topo_id:
Unique identifier for this topology
name:
Optional - should only be specified for networks that are
defined as Docker networks in input YAML
"""
network_id = 0
def __init__(
self,
generated: bool,
topo_id: Optional[str],
name: Optional[str] = None,
):
"""
Arguments:
generated:
True if this has been generated and did not exist in
input YAML
topo_id:
Unique identifier for this topology
name:
Name of the network - only applicable for networks
defined in the input YAML
"""
self.topo_id = topo_id
self.generated = generated
self.container_prefix = "xr-{}".format(Network.network_id)
Network.network_id += 1
self.name = name
self.interfaces: List[Interface] = []
def generate_name(self) -> None:
"""
Generate a name for the network. Should be called once all interfaces
have been added, and only for networks where generated is True.
"""
assert self.generated
# It is possible to get here without any interfaces in the network
# list if there were errors in the declared interfaces
if not self.interfaces:
return
# TYPE SAFETY: self.interfaces is appended to in parse_xr_network and
# find_lone_interfaces, both of which get the interfaces from a
# service. The service is set on the interface in the Service's init,
# so each interface should have service set here
if len(self.interfaces) > 1:
assert self.interfaces[0].service is not None
assert self.interfaces[1].service is not None
name = "{}-{}-{}-{}".format(
self.interfaces[0].service.service_name,
self.interfaces[0].name_abbrev,
self.interfaces[1].service.service_name,
self.interfaces[1].name_abbrev,
)
else:
assert self.interfaces[0].service is not None
name = "{}-{}".format(
self.interfaces[0].service.service_name,
self.interfaces[0].name_abbrev,
)
self.name = generate_topo_id_name(self.topo_id, name)
class XRCompose:
"""
Class to perform the main jobs of converting input XR YAML into
output full docker-compose YAML
Attributes:
topo_id:
Topology ID used for this instance, that will be suffixed to
container, network and volume names.
input_file:
Name of the input YAML file
output_file:
Name of the output YAML file
yaml:
The YAML output, which is extracted from the input file, edited,
and then rewritten to the output file
networks:
List of networks in the topology
services:
List of services in the topology
errors:
List of errors found during parsing of input YAML
image:
Image to be used in each XR container
mounts:
List of paths to be mounted into each XR container
"""
def __init__(
self,
topo_id: Optional[str] = None,
input_file: Optional[str] = None,
output_file: Optional[str] = None,
image: Optional[str] = None,
mounts: Optional[List[str]] = None,
*,
privileged: bool = False,
cgroups_v2: bool = False,
):
"""
Initialise the XRCompose class.
Raises:
InputDoesNotExistError
The input file does not exist
DockerObjectsRemainingError
There are remaining running Docker objects managed by the
specified output YAML file
Arguments:
topo_id:
Topology ID, if one was specified in input arguments.
input_file:
Input file, if one was specified in input arguments.
output_file:
Output file, if one was specified in input arguments
image:
XR image name, asspecified as a CLI argument. It may be
overriden in the YAML on a per-service basis.
mounts:
Optional list of paths to be mounted into each XR container
privileged:
Run in privileged mode
"""
self.topo_id = topo_id
self.input_file = input_file if input_file else DEFAULT_INPUT
LOGGER.debug("Input file name is %s", self.input_file)
self.output_file = output_file if output_file else DEFAULT_OUTPUT
LOGGER.debug("Output file name is %s", self.output_file)
self.mounts = mounts if mounts else []
self.privileged = privileged
self.cgroups_v2 = cgroups_v2
self.image = image
self.check_input_and_output()
# Load the input YAML
with open(self.input_file) as file:
try:
self.yaml = yaml.safe_load(file)
except yaml.YAMLError as e:
raise InputYAMLError(str(e)) from e
self.networks: List[Network] = []
self.services: List[Service] = []
self.errors: List[Exception] = []
# -------------------------------------------------------------------
# Methods related to checking input arguments
# -------------------------------------------------------------------
def check_running_docker_objects(self) -> None:
"""
Checks whether containers, networks, or volumes from the exisiting
docker-compose YAML are currently running
Raises:
DockerObjectsRemainingError
There are reamining running Docker objects managed by the
specified output YAML file
"""
# First try and load the output YAML - if it is invalid, we can't
# check its artefacts, so should just carry on with trying to generate
# new output YAML
with open(self.output_file) as file:
try:
output_yaml = yaml.safe_load(file)
except yaml.YAMLError:
LOGGER.info(
"Unable to check whether the output YAML still manages "
"running Docker objects as it contains invalid syntax."
)
return
# Check for running containers using docker-compose ps. This ensures
# that containers without a specified name are detected. Docker-compose
# is not covered by the python API. Don't include the last empty line.
try:
docker_compose_out = subprocess.check_output(
["docker-compose", "-f", self.output_file, "ps"],
universal_newlines=True,
)
except subprocess.CalledProcessError:
LOGGER.info(
"Unable to check whether the output YAML still manages "
"running Docker objects as it is not valid docker-compose"
)
return
containers = []
try:
containers = [
line.split()[0] for line in docker_compose_out.splitlines()[2:]
]
except Exception:
LOGGER.info(
"Unable to check whether the output YAML still manages "
"running Docker containers due to unexpected error"
)
# Docker-compose ps doesn't cover networks or volumes, so check these
# separately. If there is an error running the command, log it, but
# don't treat it as an error as this isn't essential functionality
try:
all_volumes = subprocess.check_output(
["docker", "volume", "ls", "--format", "{{.Name}}"],
universal_newlines=True,
).split("\n")
except subprocess.CalledProcessError as e:
LOGGER.info(
"Unable to check whether the output YAML still manages "
"running Docker volumes due to an unexpected "
"error:\n %s",
e.output,
)
volumes = [v for v in output_yaml["volumes"] if v in all_volumes]
try:
all_networks = subprocess.check_output(
["docker", "network", "ls", "--format", "{{.Name}}"],
universal_newlines=True,
).split("\n")
except subprocess.CalledProcessError as e:
LOGGER.info(
"Unable to check whether the output YAML still manages named"
"running Docker networks due to an unexpected "
"error:\n %s",
e.output,
)