Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/galaxy/containers/docker_model.py @ 0:d30785e31577 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
| author | guerler |
|---|---|
| date | Fri, 31 Jul 2020 00:18:57 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:d30785e31577 |
|---|---|
| 1 """ | |
| 2 Model objects for docker objects | |
| 3 """ | |
| 4 from __future__ import absolute_import | |
| 5 | |
| 6 import logging | |
| 7 | |
| 8 try: | |
| 9 import docker | |
| 10 except ImportError: | |
| 11 from galaxy.util.bunch import Bunch | |
| 12 docker = Bunch(errors=Bunch(NotFound=None)) | |
| 13 from six.moves import shlex_quote | |
| 14 | |
| 15 from galaxy.containers import ( | |
| 16 Container, | |
| 17 ContainerPort, | |
| 18 ContainerVolume | |
| 19 ) | |
| 20 from galaxy.util import ( | |
| 21 pretty_print_time_interval, | |
| 22 unicodify, | |
| 23 ) | |
| 24 | |
| 25 | |
| 26 CPUS_LABEL = '_galaxy_cpus' | |
| 27 IMAGE_LABEL = '_galaxy_image' | |
| 28 CPUS_CONSTRAINT = 'node.labels.' + CPUS_LABEL | |
| 29 IMAGE_CONSTRAINT = 'node.labels.' + IMAGE_LABEL | |
| 30 | |
| 31 log = logging.getLogger(__name__) | |
| 32 | |
| 33 | |
| 34 class DockerAttributeContainer(object): | |
| 35 | |
| 36 def __init__(self, members=None): | |
| 37 if members is None: | |
| 38 members = set() | |
| 39 self._members = members | |
| 40 | |
| 41 def __eq__(self, other): | |
| 42 return self.members == other.members | |
| 43 | |
| 44 def __ne__(self, other): | |
| 45 return not self.__eq__(other) | |
| 46 | |
| 47 def __hash__(self): | |
| 48 return hash(tuple(sorted([repr(x) for x in self._members]))) | |
| 49 | |
| 50 def __str__(self): | |
| 51 return ', '.join(str(x) for x in self._members) or 'None' | |
| 52 | |
| 53 def __iter__(self): | |
| 54 return iter(self._members) | |
| 55 | |
| 56 def __getitem__(self, name): | |
| 57 for member in self._members: | |
| 58 if member.name == name: | |
| 59 return member | |
| 60 else: | |
| 61 raise KeyError(name) | |
| 62 | |
| 63 def __contains__(self, item): | |
| 64 return item in self._members | |
| 65 | |
| 66 @property | |
| 67 def members(self): | |
| 68 return frozenset(self._members) | |
| 69 | |
| 70 def hash(self): | |
| 71 return hex(self.__hash__())[2:] | |
| 72 | |
| 73 def get(self, name, default): | |
| 74 try: | |
| 75 return self[name] | |
| 76 except KeyError: | |
| 77 return default | |
| 78 | |
| 79 | |
| 80 class DockerVolume(ContainerVolume): | |
| 81 @classmethod | |
| 82 def from_str(cls, as_str): | |
| 83 """Construct an instance from a string as would be passed to `docker run --volume`. | |
| 84 | |
| 85 A string in the format ``<host_path>:<mode>`` is supported for legacy purposes even though it is not valid | |
| 86 Docker volume syntax. | |
| 87 """ | |
| 88 if not as_str: | |
| 89 raise ValueError("Failed to parse Docker volume from %s" % as_str) | |
| 90 parts = as_str.split(":", 2) | |
| 91 kwds = dict(host_path=parts[0]) | |
| 92 if len(parts) == 1: | |
| 93 # auto-generated volume | |
| 94 kwds["path"] = kwds["host_path"] | |
| 95 elif len(parts) == 2: | |
| 96 # /host_path:mode is not (or is no longer?) valid Docker volume syntax | |
| 97 if parts[1] in DockerVolume.valid_modes: | |
| 98 kwds["mode"] = parts[1] | |
| 99 kwds["path"] = kwds["host_path"] | |
| 100 else: | |
| 101 kwds["path"] = parts[1] | |
| 102 elif len(parts) == 3: | |
| 103 kwds["path"] = parts[1] | |
| 104 kwds["mode"] = parts[2] | |
| 105 return cls(**kwds) | |
| 106 | |
| 107 def __str__(self): | |
| 108 volume_str = ":".join(filter(lambda x: x is not None, (self.host_path, self.path, self.mode))) | |
| 109 if "$" not in volume_str: | |
| 110 volume_for_cmd_line = shlex_quote(volume_str) | |
| 111 else: | |
| 112 # e.g. $_GALAXY_JOB_TMP_DIR:$_GALAXY_JOB_TMP_DIR:rw so don't single quote. | |
| 113 volume_for_cmd_line = '"%s"' % volume_str | |
| 114 return volume_for_cmd_line | |
| 115 | |
| 116 def to_native(self): | |
| 117 host_path = self.host_path or self.path | |
| 118 return (self.path, {host_path: {'bind': self.path, 'mode': self.mode}}) | |
| 119 | |
| 120 | |
| 121 class DockerContainer(Container): | |
| 122 | |
| 123 def __init__(self, interface, id, name=None, inspect=None): | |
| 124 super(DockerContainer, self).__init__(interface, id, name=name) | |
| 125 self._inspect = inspect | |
| 126 | |
| 127 @classmethod | |
| 128 def from_id(cls, interface, id): | |
| 129 inspect = interface.inspect(id) | |
| 130 return cls(interface, id, name=inspect['Name'], inspect=inspect) | |
| 131 | |
| 132 @property | |
| 133 def ports(self): | |
| 134 # { | |
| 135 # "NetworkSettings" : { | |
| 136 # "Ports" : { | |
| 137 # "3306/tcp" : [ | |
| 138 # { | |
| 139 # "HostIp" : "127.0.0.1", | |
| 140 # "HostPort" : "3306" | |
| 141 # } | |
| 142 # ] | |
| 143 rval = [] | |
| 144 try: | |
| 145 port_mappings = self.inspect['NetworkSettings']['Ports'] | |
| 146 except KeyError: | |
| 147 log.warning("Failed to get ports for container %s from `docker inspect` output at " | |
| 148 "['NetworkSettings']['Ports']: %s: %s", self.id, exc_info=True) | |
| 149 return None | |
| 150 for port_name in port_mappings: | |
| 151 for binding in port_mappings[port_name]: | |
| 152 rval.append(ContainerPort( | |
| 153 int(port_name.split('/')[0]), | |
| 154 port_name.split('/')[1], | |
| 155 self.address, | |
| 156 int(binding['HostPort']), | |
| 157 )) | |
| 158 return rval | |
| 159 | |
| 160 @property | |
| 161 def address(self): | |
| 162 if self._interface.host and self._interface.host.startswith('tcp://'): | |
| 163 return self._interface.host.replace('tcp://', '').split(':', 1)[0] | |
| 164 else: | |
| 165 return 'localhost' | |
| 166 | |
| 167 def is_ready(self): | |
| 168 return self.inspect['State']['Running'] | |
| 169 | |
| 170 def __eq__(self, other): | |
| 171 return self._id == other.id | |
| 172 | |
| 173 def __ne__(self, other): | |
| 174 return not self.__eq__(other) | |
| 175 | |
| 176 def __hash__(self): | |
| 177 return hash(self._id) | |
| 178 | |
| 179 @property | |
| 180 def inspect(self): | |
| 181 if not self._inspect: | |
| 182 self._inspect = self._interface.inspect(self._id) | |
| 183 return self._inspect | |
| 184 | |
| 185 | |
| 186 class DockerService(Container): | |
| 187 | |
| 188 def __init__(self, interface, id, name=None, image=None, inspect=None): | |
| 189 super(DockerService, self).__init__(interface, id, name=name) | |
| 190 self._image = image | |
| 191 self._inspect = inspect | |
| 192 self._env = {} | |
| 193 self._tasks = [] | |
| 194 if inspect: | |
| 195 self._name = name or inspect['Spec']['Name'] | |
| 196 self._image = image or inspect['Spec']['TaskTemplate']['ContainerSpec']['Image'] | |
| 197 | |
| 198 @classmethod | |
| 199 def from_cli(cls, interface, s, task_list): | |
| 200 service = cls(interface, s['ID'], name=s['NAME'], image=s['IMAGE']) | |
| 201 for task_dict in task_list: | |
| 202 if task_dict['NAME'].strip().startswith(r'\_'): | |
| 203 continue # historical task | |
| 204 service.task_add(DockerTask.from_cli(interface, task_dict, service=service)) | |
| 205 return service | |
| 206 | |
| 207 @classmethod | |
| 208 def from_id(cls, interface, id): | |
| 209 inspect = interface.service_inspect(id) | |
| 210 service = cls(interface, id, inspect=inspect) | |
| 211 for task in interface.service_tasks(service): | |
| 212 service.task_add(task) | |
| 213 return service | |
| 214 | |
| 215 @property | |
| 216 def ports(self): | |
| 217 # { | |
| 218 # "Endpoint": { | |
| 219 # "Ports": [ | |
| 220 # { | |
| 221 # "Protocol": "tcp", | |
| 222 # "TargetPort": 8888, | |
| 223 # "PublishedPort": 30000, | |
| 224 # "PublishMode": "ingress" | |
| 225 # } | |
| 226 # ] | |
| 227 rval = [] | |
| 228 try: | |
| 229 port_mappings = self.inspect['Endpoint']['Ports'] | |
| 230 except (IndexError, KeyError): | |
| 231 log.warning("Failed to get ports for container %s from `docker service inspect` output at " | |
| 232 "['Endpoint']['Ports']: %s: %s", self.id, exc_info=True) | |
| 233 return None | |
| 234 for binding in port_mappings: | |
| 235 rval.append(ContainerPort( | |
| 236 binding['TargetPort'], | |
| 237 binding['Protocol'], | |
| 238 self.address, # use the routing mesh | |
| 239 binding['PublishedPort'] | |
| 240 )) | |
| 241 return rval | |
| 242 | |
| 243 @property | |
| 244 def address(self): | |
| 245 if self._interface.host and self._interface.host.startswith('tcp://'): | |
| 246 return self._interface.host.replace('tcp://', '').split(':', 1)[0] | |
| 247 else: | |
| 248 return 'localhost' | |
| 249 | |
| 250 def is_ready(self): | |
| 251 return self.in_state('Running', 'Running') | |
| 252 | |
| 253 def __eq__(self, other): | |
| 254 return self._id == other.id | |
| 255 | |
| 256 def __ne__(self, other): | |
| 257 return not self.__eq__(other) | |
| 258 | |
| 259 def __hash__(self): | |
| 260 return hash(self._id) | |
| 261 | |
| 262 def task_add(self, task): | |
| 263 self._tasks.append(task) | |
| 264 | |
| 265 @property | |
| 266 def inspect(self): | |
| 267 if not self._inspect: | |
| 268 self._inspect = self._interface.service_inspect(self._id) | |
| 269 return self._inspect | |
| 270 | |
| 271 @property | |
| 272 def state(self): | |
| 273 """If one of this service's tasks desired state is running, return that task state, otherwise, return the state | |
| 274 of a non-running task. | |
| 275 | |
| 276 This is imperfect because it doesn't attempt to provide useful information for replicas > 1 tasks, but it suits | |
| 277 our purposes for now. | |
| 278 """ | |
| 279 state = None | |
| 280 for task in self.tasks: | |
| 281 state = task.state | |
| 282 if task.desired_state == 'running': | |
| 283 break | |
| 284 return state | |
| 285 | |
| 286 @property | |
| 287 def env(self): | |
| 288 if not self._env: | |
| 289 try: | |
| 290 for env_str in self.inspect['Spec']['TaskTemplate']['ContainerSpec']['Env']: | |
| 291 try: | |
| 292 self._env.update([env_str.split('=', 1)]) | |
| 293 except ValueError: | |
| 294 self._env[env_str] = None | |
| 295 except KeyError as exc: | |
| 296 log.debug('Cannot retrieve container environment: KeyError: %s', unicodify(exc)) | |
| 297 return self._env | |
| 298 | |
| 299 @property | |
| 300 def terminal(self): | |
| 301 """Same caveats as :meth:`state`. | |
| 302 """ | |
| 303 for task in self.tasks: | |
| 304 if task.desired_state == 'running': | |
| 305 return False | |
| 306 return True | |
| 307 | |
| 308 @property | |
| 309 def node(self): | |
| 310 """Same caveats as :meth:`state`. | |
| 311 """ | |
| 312 for task in self.tasks: | |
| 313 if task.node is not None: | |
| 314 return task.node | |
| 315 return None | |
| 316 | |
| 317 @property | |
| 318 def image(self): | |
| 319 if self._image is None: | |
| 320 self._image = self.inspect['Spec']['TaskTemplate']['ContainerSpec']['Image'] | |
| 321 return self._image | |
| 322 | |
| 323 @property | |
| 324 def cpus(self): | |
| 325 try: | |
| 326 cpus = self.inspect['Spec']['TaskTemplate']['Resources']['Limits']['NanoCPUs'] / 1000000000.0 | |
| 327 if cpus == int(cpus): | |
| 328 cpus = int(cpus) | |
| 329 return cpus | |
| 330 except KeyError: | |
| 331 return 0 | |
| 332 | |
| 333 @property | |
| 334 def constraints(self): | |
| 335 constraints = self.inspect['Spec']['TaskTemplate']['Placement'].get('Constraints', []) | |
| 336 return DockerServiceConstraints.from_constraint_string_list(constraints) | |
| 337 | |
| 338 @property | |
| 339 def tasks(self): | |
| 340 """A list of *all* tasks, including terminal ones. | |
| 341 """ | |
| 342 if not self._tasks: | |
| 343 self._tasks = [] | |
| 344 for task in self._interface.service_tasks(self): | |
| 345 self.task_add(task) | |
| 346 return self._tasks | |
| 347 | |
| 348 @property | |
| 349 def task_count(self): | |
| 350 """A count of *all* tasks, including terminal ones. | |
| 351 """ | |
| 352 return len(self.tasks) | |
| 353 | |
| 354 def in_state(self, desired, current, tasks='any'): | |
| 355 """Indicate if one of this service's tasks matches the desired state. | |
| 356 """ | |
| 357 for task in self.tasks: | |
| 358 if task.in_state(desired, current): | |
| 359 if tasks == 'any': | |
| 360 # at least 1 task in desired state | |
| 361 return True | |
| 362 elif tasks == 'all': | |
| 363 # at least 1 task not in desired state | |
| 364 return False | |
| 365 else: | |
| 366 return False if tasks == 'any' else True | |
| 367 | |
| 368 def constraint_add(self, name, op, value): | |
| 369 self._interface.service_constraint_add(self.id, name, op, value) | |
| 370 | |
| 371 def set_cpus(self): | |
| 372 self.constraint_add(CPUS_LABEL, '==', self.cpus) | |
| 373 | |
| 374 def set_image(self): | |
| 375 self.constraint_add(IMAGE_LABEL, '==', self.image) | |
| 376 | |
| 377 | |
| 378 class DockerServiceConstraint(object): | |
| 379 | |
| 380 def __init__(self, name=None, op=None, value=None): | |
| 381 self._name = name | |
| 382 self._op = op | |
| 383 self._value = value | |
| 384 | |
| 385 def __eq__(self, other): | |
| 386 return self._name == other._name and \ | |
| 387 self._op == other._op and \ | |
| 388 self._value == other._value | |
| 389 | |
| 390 def __ne__(self, other): | |
| 391 return not self.__eq__(other) | |
| 392 | |
| 393 def __hash__(self): | |
| 394 return hash((self._name, self._op, self._value)) | |
| 395 | |
| 396 def __repr__(self): | |
| 397 return '%s(%s%s%s)' % (self.__class__.__name__, self._name, self._op, self._value) | |
| 398 | |
| 399 def __str__(self): | |
| 400 return '%s%s%s' % (self._name, self._op, self._value) | |
| 401 | |
| 402 @staticmethod | |
| 403 def split_constraint_string(constraint_str): | |
| 404 constraint = (constraint_str, '', '') | |
| 405 for op in '==', '!=': | |
| 406 t = constraint_str.partition(op) | |
| 407 if len(t[0]) < len(constraint[0]): | |
| 408 constraint = t | |
| 409 if constraint[0] == constraint_str: | |
| 410 raise Exception('Unable to parse constraint string: %s' % constraint_str) | |
| 411 return [x.strip() for x in constraint] | |
| 412 | |
| 413 @classmethod | |
| 414 def from_str(cls, constraint_str): | |
| 415 name, op, value = DockerServiceConstraint.split_constraint_string(constraint_str) | |
| 416 return cls(name=name, op=op, value=value) | |
| 417 | |
| 418 @property | |
| 419 def name(self): | |
| 420 return self._name | |
| 421 | |
| 422 @property | |
| 423 def op(self): | |
| 424 return self._op | |
| 425 | |
| 426 @property | |
| 427 def value(self): | |
| 428 return self._value | |
| 429 | |
| 430 @property | |
| 431 def label(self): | |
| 432 return DockerNodeLabel( | |
| 433 name=self.name.replace('node.labels.', ''), | |
| 434 value=self.value | |
| 435 ) | |
| 436 | |
| 437 | |
| 438 class DockerServiceConstraints(DockerAttributeContainer): | |
| 439 | |
| 440 member_class = DockerServiceConstraint | |
| 441 | |
| 442 @classmethod | |
| 443 def from_constraint_string_list(cls, inspect): | |
| 444 members = [] | |
| 445 for member_str in inspect: | |
| 446 members.append(cls.member_class.from_str(member_str)) | |
| 447 return cls(members=members) | |
| 448 | |
| 449 @property | |
| 450 def labels(self): | |
| 451 return DockerNodeLabels(members=[x.label for x in self.members]) | |
| 452 | |
| 453 | |
| 454 class DockerNode(object): | |
| 455 | |
| 456 def __init__(self, interface, id=None, name=None, status=None, | |
| 457 availability=None, manager=False, inspect=None): | |
| 458 self._interface = interface | |
| 459 self._id = id | |
| 460 self._name = name | |
| 461 self._status = status | |
| 462 self._availability = availability | |
| 463 self._manager = manager | |
| 464 self._inspect = inspect | |
| 465 if inspect: | |
| 466 self._name = name or inspect['Description']['Hostname'] | |
| 467 self._status = status or inspect['Status']['State'] | |
| 468 self._availability = inspect['Spec']['Availability'] | |
| 469 self._manager = manager or inspect['Spec']['Role'] == 'manager' | |
| 470 self._tasks = [] | |
| 471 | |
| 472 @classmethod | |
| 473 def from_cli(cls, interface, n, task_list): | |
| 474 node = cls(interface, id=n['ID'], name=n['HOSTNAME'], status=n['STATUS'], | |
| 475 availability=n['AVAILABILITY'], manager=True if n['MANAGER STATUS'] else False) | |
| 476 for task_dict in task_list: | |
| 477 node.task_add(DockerTask.from_cli(interface, task_dict, node=node)) | |
| 478 return node | |
| 479 | |
| 480 @classmethod | |
| 481 def from_id(cls, interface, id): | |
| 482 inspect = interface.node_inspect(id) | |
| 483 node = cls(interface, id, inspect=inspect) | |
| 484 for task in interface.node_tasks(node): | |
| 485 node.task_add(task) | |
| 486 return node | |
| 487 | |
| 488 def task_add(self, task): | |
| 489 self._tasks.append(task) | |
| 490 | |
| 491 @property | |
| 492 def id(self): | |
| 493 return self._id | |
| 494 | |
| 495 @property | |
| 496 def name(self): | |
| 497 return self._name | |
| 498 | |
| 499 @property | |
| 500 def version(self): | |
| 501 # this changes on update so don't cache | |
| 502 return self._interface.node_inspect(self._id or self._name)['Version']['Index'] | |
| 503 | |
| 504 @property | |
| 505 def inspect(self): | |
| 506 if not self._inspect: | |
| 507 self._inspect = self._interface.node_inspect(self._id or self._name) | |
| 508 return self._inspect | |
| 509 | |
| 510 @property | |
| 511 def state(self): | |
| 512 return ('%s-%s' % (self._status, self._availability)).lower() | |
| 513 | |
| 514 @property | |
| 515 def cpus(self): | |
| 516 return self.inspect['Description']['Resources']['NanoCPUs'] / 1000000000 | |
| 517 | |
| 518 @property | |
| 519 def labels(self): | |
| 520 labels = self.inspect['Spec'].get('Labels', {}) or {} | |
| 521 return DockerNodeLabels.from_label_dictionary(labels) | |
| 522 | |
| 523 def label_add(self, label, value): | |
| 524 self._interface.node_update(self.id, label_add={label: value}) | |
| 525 | |
| 526 @property | |
| 527 def labels_as_constraints(self): | |
| 528 constraints_strings = [x.constraint_string for x in self.labels] | |
| 529 return DockerServiceConstraints.from_constraint_string_list(constraints_strings) | |
| 530 | |
| 531 def set_labels_for_constraints(self, constraints): | |
| 532 for label in self._constraints_to_label_args(constraints): | |
| 533 if label not in self.labels: | |
| 534 log.info("setting node '%s' label '%s' to '%s'", self.name, label.name, label.value) | |
| 535 self.label_add(label.name, label.value) | |
| 536 | |
| 537 def _constraints_to_label_args(self, constraints): | |
| 538 constraints = filter(lambda x: x.name.startswith('node.labels.') and x.op == '==', constraints) | |
| 539 labels = map(lambda x: DockerNodeLabel(name=x.name.replace('node.labels.', '', 1), value=x.value), constraints) | |
| 540 return labels | |
| 541 | |
| 542 @property | |
| 543 def tasks(self): | |
| 544 """A list of *all* tasks, including terminal ones. | |
| 545 """ | |
| 546 if not self._tasks: | |
| 547 self._tasks = [] | |
| 548 for task in self._interface.node_tasks(self): | |
| 549 self.task_add(task) | |
| 550 return self._tasks | |
| 551 | |
| 552 @property | |
| 553 def non_terminal_tasks(self): | |
| 554 r = [] | |
| 555 for task in self.tasks: | |
| 556 # ensure the task has a service - it is possible for "phantom" tasks to exist (service is removed, no | |
| 557 # container is running, but the task still shows up in the node's task list) | |
| 558 if not task.terminal and task.service is not None: | |
| 559 r.append(task) | |
| 560 return r | |
| 561 | |
| 562 @property | |
| 563 def task_count(self): | |
| 564 """A count of *all* tasks, including terminal ones. | |
| 565 """ | |
| 566 return len(self.tasks) | |
| 567 | |
| 568 def in_state(self, status, availability): | |
| 569 return self._status.lower() == status.lower() and self._availability.lower() == availability.lower() | |
| 570 | |
| 571 def is_ok(self): | |
| 572 return self.in_state('Ready', 'Active') | |
| 573 | |
| 574 def is_managed(self): | |
| 575 return not self._manager | |
| 576 | |
| 577 def destroyable(self): | |
| 578 return not self._manager and self.is_ok() and self.task_count == 0 | |
| 579 | |
| 580 def drain(self): | |
| 581 self._interface.node_update(self.id, availability='drain') | |
| 582 | |
| 583 | |
| 584 class DockerNodeLabel(object): | |
| 585 | |
| 586 def __init__(self, name=None, value=None): | |
| 587 self._name = name | |
| 588 self._value = value | |
| 589 | |
| 590 def __eq__(self, other): | |
| 591 return self._name == other._name and \ | |
| 592 self._value == other._value | |
| 593 | |
| 594 def __ne__(self, other): | |
| 595 return not self.__eq__(other) | |
| 596 | |
| 597 def __hash__(self): | |
| 598 return hash((self._name, self._value)) | |
| 599 | |
| 600 def __repr__(self): | |
| 601 return '%s(%s: %s)' % (self.__class__.__name__, self._name, self._value) | |
| 602 | |
| 603 def __str__(self): | |
| 604 return '%s: %s' % (self._name, self._value) | |
| 605 | |
| 606 @property | |
| 607 def name(self): | |
| 608 return self._name | |
| 609 | |
| 610 @property | |
| 611 def value(self): | |
| 612 return self._value | |
| 613 | |
| 614 @property | |
| 615 def constraint_string(self): | |
| 616 return 'node.labels.{name}=={value}'.format(name=self.name, value=self.value) | |
| 617 | |
| 618 @property | |
| 619 def constraint(self): | |
| 620 return DockerServiceConstraint( | |
| 621 name='node.labels.{name}'.format(name=self.name), | |
| 622 op='==', | |
| 623 value=self.value | |
| 624 ) | |
| 625 | |
| 626 | |
| 627 class DockerNodeLabels(DockerAttributeContainer): | |
| 628 | |
| 629 member_class = DockerNodeLabel | |
| 630 | |
| 631 @classmethod | |
| 632 def from_label_dictionary(cls, inspect): | |
| 633 members = [] | |
| 634 for k, v in inspect.items(): | |
| 635 members.append(cls.member_class(name=k, value=v)) | |
| 636 return cls(members=members) | |
| 637 | |
| 638 @property | |
| 639 def constraints(self): | |
| 640 return DockerServiceConstraints(members=[x.constraint for x in self.members]) | |
| 641 | |
| 642 | |
| 643 class DockerTask(object): | |
| 644 | |
| 645 # these are the possible *current* state terminal states | |
| 646 terminal_states = ( | |
| 647 'shutdown', # this is normally only a desired state but I've seen a task with it as current as well | |
| 648 'complete', | |
| 649 'failed', | |
| 650 'rejected', | |
| 651 'orphaned', | |
| 652 ) | |
| 653 | |
| 654 def __init__(self, interface, id=None, name=None, image=None, desired_state=None, | |
| 655 state=None, error=None, ports=None, service=None, node=None): | |
| 656 self._interface = interface | |
| 657 self._id = id | |
| 658 self._name = name | |
| 659 self._image = image | |
| 660 self._desired_state = desired_state | |
| 661 self._state = state | |
| 662 self._error = error | |
| 663 self._ports = ports | |
| 664 self._service = service | |
| 665 self._node = node | |
| 666 self._inspect = None | |
| 667 | |
| 668 @classmethod | |
| 669 def from_cli(cls, interface, t, service=None, node=None): | |
| 670 state = t['CURRENT STATE'].split()[0] | |
| 671 return cls(interface, id=t['ID'], name=t['NAME'], image=t['IMAGE'], | |
| 672 desired_state=t['DESIRED STATE'], state=state, error=t['ERROR'], | |
| 673 ports=t['PORTS'], service=service, node=node) | |
| 674 | |
| 675 @classmethod | |
| 676 def from_api(cls, interface, t, service=None, node=None): | |
| 677 service = service or interface.service(id=t.get('ServiceID')) | |
| 678 node = node or interface.node(id=t.get('NodeID')) | |
| 679 if service: | |
| 680 name = service.name + '.' + str(t['Slot']) | |
| 681 else: | |
| 682 name = t['ID'] | |
| 683 image = t['Spec']['ContainerSpec']['Image'].split('@', 1)[0], # remove pin | |
| 684 return cls(interface, id=t['ID'], name=name, image=image, desired_state=t['DesiredState'], | |
| 685 state=t['Status']['State'], ports=t['Status']['PortStatus'], error=t['Status']['Message'], | |
| 686 service=service, node=node) | |
| 687 | |
| 688 @property | |
| 689 def id(self): | |
| 690 return self._id | |
| 691 | |
| 692 @property | |
| 693 def name(self): | |
| 694 return self._name | |
| 695 | |
| 696 @property | |
| 697 def inspect(self): | |
| 698 if not self._inspect: | |
| 699 try: | |
| 700 self._inspect = self._interface.task_inspect(self._id) | |
| 701 except docker.errors.NotFound: | |
| 702 # This shouldn't be possible, appears to be some kind of Swarm bug (the node claims to have a task that | |
| 703 # does not actually exist anymore, nor does its service exist). | |
| 704 log.error('Task could not be inspected because Docker claims it does not exist: %s (%s)', | |
| 705 self.name, self.id) | |
| 706 return None | |
| 707 return self._inspect | |
| 708 | |
| 709 @property | |
| 710 def slot(self): | |
| 711 try: | |
| 712 return self.inspect['Slot'] | |
| 713 except TypeError: | |
| 714 return None | |
| 715 | |
| 716 @property | |
| 717 def node(self): | |
| 718 if not self._node: | |
| 719 try: | |
| 720 self._node = self._interface.node(id=self.inspect['NodeID']) | |
| 721 except TypeError: | |
| 722 return None | |
| 723 return self._node | |
| 724 | |
| 725 @property | |
| 726 def service(self): | |
| 727 if not self._service: | |
| 728 try: | |
| 729 self._service = self._interface.service(id=self.inspect['ServiceID']) | |
| 730 except TypeError: | |
| 731 return None | |
| 732 return self._service | |
| 733 | |
| 734 @property | |
| 735 def cpus(self): | |
| 736 try: | |
| 737 cpus = self.inspect['Spec']['Resources']['Reservations']['NanoCPUs'] / 1000000000.0 | |
| 738 if cpus == int(cpus): | |
| 739 cpus = int(cpus) | |
| 740 return cpus | |
| 741 except TypeError: | |
| 742 return None | |
| 743 except KeyError: | |
| 744 return 0 | |
| 745 | |
| 746 @property | |
| 747 def state(self): | |
| 748 return ('%s-%s' % (self._desired_state, self._state)).lower() | |
| 749 | |
| 750 @property | |
| 751 def current_state(self): | |
| 752 try: | |
| 753 return self._state.lower() | |
| 754 except TypeError: | |
| 755 log.warning("Current state of %s (%s) is not a string: %s", self.name, self.id, str(self._state)) | |
| 756 return None | |
| 757 | |
| 758 @property | |
| 759 def current_state_time(self): | |
| 760 # Docker API returns a stamp w/ higher second precision than Python takes | |
| 761 try: | |
| 762 stamp = self.inspect['Status']['Timestamp'] | |
| 763 except TypeError: | |
| 764 return None | |
| 765 return pretty_print_time_interval(time=stamp[:stamp.index('.') + 7], precise=True, utc=stamp[-1] == 'Z') | |
| 766 | |
| 767 @property | |
| 768 def desired_state(self): | |
| 769 try: | |
| 770 return self._desired_state.lower() | |
| 771 except TypeError: | |
| 772 log.warning("Desired state of %s (%s) is not a string: %s", self.name, self.id, str(self._desired_state)) | |
| 773 return None | |
| 774 | |
| 775 @property | |
| 776 def terminal(self): | |
| 777 return self.desired_state == 'shutdown' and self.current_state in self.terminal_states | |
| 778 | |
| 779 def in_state(self, desired, current): | |
| 780 return self.desired_state == desired.lower() and self.current_state == current.lower() |
