|
11 | 11 | from pytest_mh.conn import ProcessResult |
12 | 12 |
|
13 | 13 | from ..hosts.samba import SambaHost |
14 | | -from ..misc import attrs_parse, to_list_of_strings |
| 14 | +from ..misc import attrs_parse, ip_version, to_list_of_strings |
15 | 15 | from ..utils.ldap import LDAPRecordAttributes |
16 | 16 | from .base import BaseLinuxLDAPRole, BaseObject, DeleteAttribute |
17 | 17 | from .generic import GenericPasswordPolicy |
|
28 | 28 | "SambaAutomount", |
29 | 29 | "SambaSudoRule", |
30 | 30 | "SambaGPO", |
| 31 | + "SambaDNSServer", |
| 32 | + "SambaDNSZone", |
31 | 33 | ] |
32 | 34 |
|
33 | 35 |
|
@@ -301,6 +303,38 @@ def test_example(client: Client, samba: Samba): |
301 | 303 | """ |
302 | 304 | return SambaComputer(self, name) |
303 | 305 |
|
| 306 | + def dns(self) -> SambaDNSServer: |
| 307 | + """ |
| 308 | + Get DNS server object. |
| 309 | +
|
| 310 | + Get methods use dig and is parsed by jc. The data from jc contains several nested dict, |
| 311 | + but two are returned as a tuple, ``answer, authority``. |
| 312 | +
|
| 313 | + .. code-block:: python |
| 314 | + :caption: Example usage |
| 315 | +
|
| 316 | + # Create forward zone and add forward record |
| 317 | + zone = samba.dns().zone("example.test").create() |
| 318 | + zone.add_record("client", "172.16.200.15") |
| 319 | +
|
| 320 | + # Create reverse zone and add reverse record |
| 321 | + zone = samba.dns().zone("10.0.10.in-addr.arpa").create() |
| 322 | + zone.add_ptr_record("client.example.test", 15) |
| 323 | +
|
| 324 | + # Add forward record to default domain |
| 325 | + samba.dns().zone(samba.domain).add_record("client", "1.2.3.4") |
| 326 | +
|
| 327 | + # Add a global forwarder |
| 328 | + samba.dns().add_forwarder("1.1.1.1") |
| 329 | +
|
| 330 | + # Remove a global forwarder |
| 331 | + samba.dns().remove_forwarder("1.1.1.1") |
| 332 | +
|
| 333 | + # Clear all forwarders |
| 334 | + samba.dns().clear_forwarders() |
| 335 | + """ |
| 336 | + return SambaDNSServer(self) |
| 337 | + |
304 | 338 | def gpo(self, name: str) -> SambaGPO: |
305 | 339 | """ |
306 | 340 | Get group policy object. |
@@ -887,8 +921,8 @@ class SambaComputer(SambaObject): |
887 | 921 |
|
888 | 922 | def __init__(self, role: Samba, name: str) -> None: |
889 | 923 | """ |
890 | | - :param role: AD role object. |
891 | | - :type role: AD |
| 924 | + :param role: Samba role object. |
| 925 | + :type role: Samba |
892 | 926 | :param name: Computer name. |
893 | 927 | :type name: str |
894 | 928 | """ |
@@ -1172,6 +1206,206 @@ def lockout(self, duration: int, attempts: int) -> SambaPasswordPolicy: |
1172 | 1206 | return self |
1173 | 1207 |
|
1174 | 1208 |
|
| 1209 | +class SambaDNSServer(BaseObject[SambaHost, Samba]): |
| 1210 | + """ |
| 1211 | + DNS management utilities. |
| 1212 | + """ |
| 1213 | + |
| 1214 | + def __init__(self, role: Samba): |
| 1215 | + """ |
| 1216 | + :param role: Samba host object. |
| 1217 | + :type role: SambaHost |
| 1218 | + """ |
| 1219 | + super().__init__(role) |
| 1220 | + |
| 1221 | + self.domain: str = role.domain |
| 1222 | + """Domain name.""" |
| 1223 | + |
| 1224 | + self.server: str = role.server |
| 1225 | + """Server name.""" |
| 1226 | + |
| 1227 | + self.naming_context: str = role.naming_context |
| 1228 | + """Naming context.""" |
| 1229 | + |
| 1230 | + self.credentials: str = f" --username={self.role.host.adminuser} --password={self.role.host.adminpw}" |
| 1231 | + """Credentials to manage GPOs.""" |
| 1232 | + |
| 1233 | + self.smb_conf: str = "/etc/samba/smb.conf" |
| 1234 | + |
| 1235 | + def zone(self, name: str) -> SambaDNSZone: |
| 1236 | + """ |
| 1237 | + Get SambaDNSZone object. |
| 1238 | +
|
| 1239 | + :param name: Zone name. |
| 1240 | + :type name: str |
| 1241 | + :return: SambaDNSZone object. |
| 1242 | + :rtype: SambaDNSZone |
| 1243 | + """ |
| 1244 | + return SambaDNSZone(self.role, name) |
| 1245 | + |
| 1246 | + def get_forwarders(self) -> list[str] | None: |
| 1247 | + """ |
| 1248 | + Get DNS global forwarders. |
| 1249 | +
|
| 1250 | + Global forwarders are configured in /etc/smb.conf |
| 1251 | +
|
| 1252 | + :return: DNS global forwarders. |
| 1253 | + :rtype: list[str] |
| 1254 | + """ |
| 1255 | + result = [line.strip() for line in self.host.fs.read(self.smb_conf).split("\n")] |
| 1256 | + if result is not None and isinstance(result, list): |
| 1257 | + for i in result: |
| 1258 | + if "dns forwarder" in i: |
| 1259 | + # The additional split is to support more than one server |
| 1260 | + return i.split("=")[1].strip().split(" ") |
| 1261 | + return None |
| 1262 | + |
| 1263 | + def add_forwarder(self, ip_address: str) -> SambaDNSServer: |
| 1264 | + """ |
| 1265 | + Add a DNS server forwarder. |
| 1266 | +
|
| 1267 | + :param ip_address: IP address. |
| 1268 | + :type ip_address: str |
| 1269 | + :return: Self. |
| 1270 | + :rtype: SambaDNSServer |
| 1271 | + """ |
| 1272 | + self.host.fs.backup(self.smb_conf) |
| 1273 | + self.host.fs.sed(f"s/dns forwarder = .*/ & {ip_address}/", self.smb_conf, ["-i"]) |
| 1274 | + self.host.svc.reload("samba.service") |
| 1275 | + return self |
| 1276 | + |
| 1277 | + def remove_forwarder(self, ip_address: str) -> None: |
| 1278 | + """ |
| 1279 | + Remove a DNS server forwarder. |
| 1280 | +
|
| 1281 | + :param ip_address: IP address. |
| 1282 | + :type ip_address: str |
| 1283 | + """ |
| 1284 | + ip_address = ip_address.replace(".", "\\.").strip() |
| 1285 | + self.host.fs.backup(self.smb_conf) |
| 1286 | + self.host.fs.sed(f"/dns forwarder/s/ {ip_address}//", self.smb_conf, ["-i"]) |
| 1287 | + self.host.svc.reload("samba.service") |
| 1288 | + |
| 1289 | + def clear_forwarders(self) -> None: |
| 1290 | + """ |
| 1291 | + Clear all DNS server forwarders. |
| 1292 | +
|
| 1293 | + Samba has one global forwarder enabled by default. |
| 1294 | + """ |
| 1295 | + forwarders = self.get_forwarders() |
| 1296 | + |
| 1297 | + if isinstance(forwarders, list) and not None: |
| 1298 | + for forwarder in forwarders: |
| 1299 | + self.remove_forwarder(forwarder) |
| 1300 | + |
| 1301 | + def list_zones(self) -> list[str]: |
| 1302 | + """ |
| 1303 | + List zones. |
| 1304 | +
|
| 1305 | + :return: List of zones. |
| 1306 | + :rtype: list[str] |
| 1307 | + """ |
| 1308 | + result = self.host.conn.run(f"samba-tool dns zonelist {self.server} {self.credentials}").stdout_lines |
| 1309 | + result = [i for i in result if "pszZoneName" in i] |
| 1310 | + result = [z.split(":")[1].strip() for z in result] |
| 1311 | + |
| 1312 | + return result |
| 1313 | + |
| 1314 | + |
| 1315 | +class SambaDNSZone(SambaDNSServer): |
| 1316 | + """ |
| 1317 | + DNS zone management. |
| 1318 | + """ |
| 1319 | + |
| 1320 | + def __init__(self, role: Samba, name: str): |
| 1321 | + """ |
| 1322 | + :param role: Samba host object. |
| 1323 | + :type role: SambaHost |
| 1324 | + :param name: DNS zone name. |
| 1325 | + :type name: str |
| 1326 | + """ |
| 1327 | + super().__init__(role) |
| 1328 | + |
| 1329 | + self.zone_name: str = name |
| 1330 | + """Zone name.""" |
| 1331 | + |
| 1332 | + def create(self) -> SambaDNSZone: |
| 1333 | + """ |
| 1334 | + Create new zone. |
| 1335 | +
|
| 1336 | + :return: SambaDNSServer object. |
| 1337 | + :rtype: SambaDNSServer |
| 1338 | + """ |
| 1339 | + self.host.conn.run(f"samba-tool dns zonecreate {self.server} {self.zone_name} {self.credentials}") |
| 1340 | + return self |
| 1341 | + |
| 1342 | + def delete(self) -> None: |
| 1343 | + """ |
| 1344 | + Delete zone. |
| 1345 | + """ |
| 1346 | + self.host.conn.run(f"samba-tool dns zonedelete {self.server} {self.zone_name} {self.credentials}") |
| 1347 | + |
| 1348 | + def add_record(self, name: str, data: str) -> SambaDNSZone: |
| 1349 | + """ |
| 1350 | + Add DNS record. |
| 1351 | +
|
| 1352 | + If ``data`` is a str, a forward record will be added. |
| 1353 | + If an integer a reverse record will be added. |
| 1354 | +
|
| 1355 | + :param name: Record name. |
| 1356 | + :type name: str | int |
| 1357 | + :param data: Record data. |
| 1358 | + :type data: str |
| 1359 | + :return: SambaDNSZone object. |
| 1360 | + :rtype: SambaDNSZone |
| 1361 | + """ |
| 1362 | + args = "" |
| 1363 | + |
| 1364 | + if isinstance(data, int): |
| 1365 | + args = f" {name} PTR {str(data)} {self.credentials}" |
| 1366 | + elif isinstance(data, str) and ip_version(data) == 4: |
| 1367 | + args = f" {name} A {data} {self.credentials}" |
| 1368 | + elif isinstance(data, str) and ip_version(data) == 6: |
| 1369 | + args = f" {name} AAAA {data} {self.credentials}" |
| 1370 | + |
| 1371 | + self.host.conn.run(f"samba-tool dns add {self.server} {self.zone_name} {args}") |
| 1372 | + return self |
| 1373 | + |
| 1374 | + def delete_record(self, name: str) -> None: |
| 1375 | + """ |
| 1376 | + Delete DNS record. |
| 1377 | +
|
| 1378 | + :param name: Name of the record. |
| 1379 | + :type name: str |
| 1380 | + """ |
| 1381 | + if "in-addr" in self.zone_name: |
| 1382 | + record_type = "PTR" |
| 1383 | + data = self.host.conn.run(f"dig -x +short {name}").stdout.strip() |
| 1384 | + else: |
| 1385 | + data = self.host.conn.run(f"dig +short {name}").stdout.strip() |
| 1386 | + record_type = "AAAA" if ":" in data else "A" |
| 1387 | + |
| 1388 | + self.role.host.conn.run( |
| 1389 | + f"samba-tool dns delete " |
| 1390 | + f"{self.server} {self.zone_name} " |
| 1391 | + f"{name} {record_type} {data} " |
| 1392 | + f"{self.credentials}" |
| 1393 | + ) |
| 1394 | + |
| 1395 | + def print(self) -> str: |
| 1396 | + """ |
| 1397 | + Prints all dns records in a zone as text. |
| 1398 | +
|
| 1399 | + :return: Print zone data. |
| 1400 | + :rtype: str |
| 1401 | + """ |
| 1402 | + result = self.host.conn.run( |
| 1403 | + f"samba-tool dns query {self.server} {self.zone_name} @ ALL {self.credentials}" |
| 1404 | + ).stdout |
| 1405 | + |
| 1406 | + return result |
| 1407 | + |
| 1408 | + |
1175 | 1409 | SambaOrganizationalUnit: TypeAlias = LDAPOrganizationalUnit[SambaHost, Samba] |
1176 | 1410 | SambaAutomount: TypeAlias = LDAPAutomount[SambaHost, Samba] |
1177 | 1411 | SambaSudoRule: TypeAlias = LDAPSudoRule[SambaHost, Samba, SambaUser, SambaGroup] |
|
0 commit comments