]>
Commit | Line | Data |
---|---|---|
b3b6e05e | 1 | from collections import OrderedDict |
f67539c2 | 2 | import errno |
20effc67 TL |
3 | import re |
4 | from typing import Optional, List, Any, Dict | |
5 | ||
6 | ||
7 | def assert_valid_host(name: str) -> None: | |
8 | p = re.compile('^[a-zA-Z0-9-]+$') | |
9 | try: | |
10 | assert len(name) <= 250, 'name is too long (max 250 chars)' | |
11 | for part in name.split('.'): | |
12 | assert len(part) > 0, '.-delimited name component must not be empty' | |
13 | assert len(part) <= 63, '.-delimited name component must not be more than 63 chars' | |
14 | assert p.match(part), 'name component must include only a-z, 0-9, and -' | |
15 | except AssertionError as e: | |
2a845540 | 16 | raise SpecValidationError(str(e) + f'. Got "{name}"') |
f6b5b4d7 TL |
17 | |
18 | ||
f38dd50b TL |
19 | def assert_valid_oob(oob: Dict[str, str]) -> None: |
20 | fields = ['username', 'password'] | |
21 | try: | |
22 | for field in fields: | |
23 | assert field in oob.keys() | |
24 | except AssertionError as e: | |
25 | raise SpecValidationError(str(e)) | |
26 | ||
27 | ||
f67539c2 TL |
28 | class SpecValidationError(Exception): |
29 | """ | |
30 | Defining an exception here is a bit problematic, cause you cannot properly catch it, | |
31 | if it was raised in a different mgr module. | |
32 | """ | |
33 | def __init__(self, | |
34 | msg: str, | |
35 | errno: int = -errno.EINVAL): | |
36 | super(SpecValidationError, self).__init__(msg) | |
37 | self.errno = errno | |
38 | ||
39 | ||
f6b5b4d7 TL |
40 | class HostSpec(object): |
41 | """ | |
42 | Information about hosts. Like e.g. ``kubectl get nodes`` | |
43 | """ | |
44 | def __init__(self, | |
b3b6e05e TL |
45 | hostname: str, |
46 | addr: Optional[str] = None, | |
47 | labels: Optional[List[str]] = None, | |
48 | status: Optional[str] = None, | |
49 | location: Optional[Dict[str, str]] = None, | |
f38dd50b | 50 | oob: Optional[Dict[str, str]] = None, |
f6b5b4d7 TL |
51 | ): |
52 | self.service_type = 'host' | |
53 | ||
54 | #: the bare hostname on the host. Not the FQDN. | |
55 | self.hostname = hostname # type: str | |
56 | ||
57 | #: DNS name or IP address to reach it | |
58 | self.addr = addr or hostname # type: str | |
59 | ||
60 | #: label(s), if any | |
61 | self.labels = labels or [] # type: List[str] | |
62 | ||
63 | #: human readable status | |
64 | self.status = status or '' # type: str | |
65 | ||
b3b6e05e TL |
66 | self.location = location |
67 | ||
f38dd50b TL |
68 | #: oob details, if provided |
69 | self.oob = oob | |
70 | ||
20effc67 TL |
71 | def validate(self) -> None: |
72 | assert_valid_host(self.hostname) | |
f38dd50b TL |
73 | if self.oob: |
74 | assert_valid_oob(self.oob) | |
20effc67 | 75 | |
b3b6e05e TL |
76 | def to_json(self) -> Dict[str, Any]: |
77 | r: Dict[str, Any] = { | |
f6b5b4d7 TL |
78 | 'hostname': self.hostname, |
79 | 'addr': self.addr, | |
b3b6e05e | 80 | 'labels': list(OrderedDict.fromkeys((self.labels))), |
f6b5b4d7 TL |
81 | 'status': self.status, |
82 | } | |
b3b6e05e TL |
83 | if self.location: |
84 | r['location'] = self.location | |
f38dd50b TL |
85 | if self.oob: |
86 | r['oob'] = self.oob | |
b3b6e05e | 87 | return r |
f6b5b4d7 TL |
88 | |
89 | @classmethod | |
f67539c2 TL |
90 | def from_json(cls, host_spec: dict) -> 'HostSpec': |
91 | host_spec = cls.normalize_json(host_spec) | |
b3b6e05e TL |
92 | _cls = cls( |
93 | host_spec['hostname'], | |
94 | host_spec['addr'] if 'addr' in host_spec else None, | |
95 | list(OrderedDict.fromkeys( | |
96 | host_spec['labels'])) if 'labels' in host_spec else None, | |
97 | host_spec['status'] if 'status' in host_spec else None, | |
98 | host_spec.get('location'), | |
f38dd50b | 99 | host_spec['oob'] if 'oob' in host_spec else None, |
b3b6e05e | 100 | ) |
f6b5b4d7 TL |
101 | return _cls |
102 | ||
f67539c2 TL |
103 | @staticmethod |
104 | def normalize_json(host_spec: dict) -> dict: | |
105 | labels = host_spec.get('labels') | |
b3b6e05e TL |
106 | if labels is not None: |
107 | if isinstance(labels, str): | |
108 | host_spec['labels'] = [labels] | |
109 | elif ( | |
110 | not isinstance(labels, list) | |
111 | or any(not isinstance(v, str) for v in labels) | |
112 | ): | |
113 | raise SpecValidationError( | |
114 | f'Labels ({labels}) must be a string or list of strings' | |
115 | ) | |
116 | ||
117 | loc = host_spec.get('location') | |
118 | if loc is not None: | |
119 | if ( | |
120 | not isinstance(loc, dict) | |
121 | or any(not isinstance(k, str) for k in loc.keys()) | |
122 | or any(not isinstance(v, str) for v in loc.values()) | |
123 | ): | |
124 | raise SpecValidationError( | |
125 | f'Location ({loc}) must be a dictionary of strings to strings' | |
126 | ) | |
127 | ||
f67539c2 TL |
128 | return host_spec |
129 | ||
130 | def __repr__(self) -> str: | |
f6b5b4d7 TL |
131 | args = [self.hostname] # type: List[Any] |
132 | if self.addr is not None: | |
133 | args.append(self.addr) | |
134 | if self.labels: | |
135 | args.append(self.labels) | |
136 | if self.status: | |
137 | args.append(self.status) | |
b3b6e05e TL |
138 | if self.location: |
139 | args.append(self.location) | |
f6b5b4d7 TL |
140 | |
141 | return "HostSpec({})".format(', '.join(map(repr, args))) | |
142 | ||
f67539c2 | 143 | def __str__(self) -> str: |
f6b5b4d7 TL |
144 | if self.hostname != self.addr: |
145 | return f'{self.hostname} ({self.addr})' | |
146 | return self.hostname | |
147 | ||
f67539c2 | 148 | def __eq__(self, other: Any) -> bool: |
f6b5b4d7 | 149 | # Let's omit `status` for the moment, as it is still the very same host. |
39ae355f TL |
150 | if not isinstance(other, HostSpec): |
151 | return NotImplemented | |
f6b5b4d7 | 152 | return self.hostname == other.hostname and \ |
39ae355f TL |
153 | self.addr == other.addr and \ |
154 | sorted(self.labels) == sorted(other.labels) and \ | |
155 | self.location == other.location |